fix : open api , 공지사항 반응형 퍼블리싱
This commit is contained in:
@@ -693,7 +693,7 @@ hr {
|
||||
--accent-orange: #FF6B6B;
|
||||
--accent-green: #6BCF7F;
|
||||
--accent-purple: #A78BFA;
|
||||
--accent-color:#efdcb2;
|
||||
--accent-color: #efdcb2;
|
||||
--accent-light: #E9F9FF;
|
||||
--text-dark: #1A1A2E;
|
||||
--text-gray: #64748B;
|
||||
@@ -1490,7 +1490,7 @@ body.design-survey-active .global-header {
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
gap: 36px;
|
||||
gap: 28px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1545,7 +1545,7 @@ body.design-survey-active .global-header {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--text-gray);
|
||||
padding: 8px 0;
|
||||
padding: 8px;
|
||||
position: relative;
|
||||
transition: var(--transition-smooth);
|
||||
}
|
||||
@@ -6028,7 +6028,7 @@ select.form-control {
|
||||
width: 100%;
|
||||
}
|
||||
.board-table .board-table-row .row-cell--title .notice-title-link:hover {
|
||||
text-decoration: underline;
|
||||
font-weight: 600;
|
||||
}
|
||||
.board-table .board-table-row .row-cell--title .notice-title-link .notice-number {
|
||||
display: none;
|
||||
@@ -6521,7 +6521,8 @@ select.form-control {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.pagination .pagination-btn i, .pagination .pagination-btn svg {
|
||||
.pagination .pagination-btn i,
|
||||
.pagination .pagination-btn svg {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
@@ -10226,32 +10227,38 @@ body.index-page-body {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.api-market-container .service-sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
width: 280px;
|
||||
margin: 0;
|
||||
z-index: 1000;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
|
||||
padding: 80px 24px 30px;
|
||||
}
|
||||
.api-market-container .service-sidebar.mobile-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
.api-market-container .service-sidebar .service-nav {
|
||||
flex-direction: column !important;
|
||||
gap: 16px !important;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
.api-market-container {
|
||||
/*
|
||||
.service-sidebar {
|
||||
@media (max-width: 768px) {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
width: 280px;
|
||||
margin: 0;
|
||||
z-index: 1000;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
|
||||
padding: 80px 24px 30px; // Clear header height if any
|
||||
|
||||
&.mobile-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.service-nav {
|
||||
flex-direction: column !important;
|
||||
gap: 16px !important;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
.api-market-hero {
|
||||
@@ -10471,6 +10478,7 @@ body.index-page-body {
|
||||
|
||||
.api-market-content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -10491,35 +10499,7 @@ body.index-page-body {
|
||||
}
|
||||
|
||||
.api-mobile-toggle {
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
color: #FFFFFF;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 8px 24px rgba(75, 155, 255, 0.15);
|
||||
z-index: 1051;
|
||||
transition: all 0.3s ease;
|
||||
background-color: #0049b4;
|
||||
}
|
||||
.api-mobile-toggle:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 16px 40px rgba(75, 155, 255, 0.2);
|
||||
}
|
||||
.api-mobile-toggle:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.api-mobile-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.api-market-header {
|
||||
@@ -11138,9 +11118,12 @@ body.index-page-body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.api-market-content {
|
||||
max-width: 100vw;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.api-card {
|
||||
height: 285px;
|
||||
}
|
||||
}
|
||||
.login-page {
|
||||
display: flex;
|
||||
@@ -18364,6 +18347,14 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
color: #0a4ea3;
|
||||
border: 1px solid #b3d4ff;
|
||||
}
|
||||
.notice-type-badge.lg {
|
||||
height: 28px;
|
||||
width: auto;
|
||||
min-width: 50px;
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.notice-detail-title {
|
||||
font-size: 28px;
|
||||
@@ -18421,22 +18412,27 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
.notice-attachment-link .attachment-filename {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.notice-detail-incident-info {
|
||||
.notice-detail-incident-info,
|
||||
.notice-detail-affected-info {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.notice-detail-incident-info .detail-table {
|
||||
.notice-detail-incident-info .detail-table,
|
||||
.notice-detail-affected-info .detail-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
border-top: none;
|
||||
border-bottom: 1px solid #e3e8f0;
|
||||
}
|
||||
.notice-detail-incident-info .detail-table tr {
|
||||
.notice-detail-incident-info .detail-table tr,
|
||||
.notice-detail-affected-info .detail-table tr {
|
||||
border-bottom: 1px solid #e3e8f0;
|
||||
}
|
||||
.notice-detail-incident-info .detail-table th {
|
||||
.notice-detail-incident-info .detail-table th,
|
||||
.notice-detail-affected-info .detail-table th {
|
||||
background: #f9f9f9;
|
||||
height: 43px;
|
||||
font-weight: 500;
|
||||
@@ -18444,12 +18440,114 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
text-align: center;
|
||||
color: #000;
|
||||
}
|
||||
.notice-detail-incident-info .detail-table td {
|
||||
padding-left: 19px;
|
||||
.notice-detail-incident-info .detail-table td,
|
||||
.notice-detail-affected-info .detail-table td {
|
||||
padding: 10px 19px;
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
word-break: break-all;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.notice-detail-incident-info .detail-table,
|
||||
.notice-detail-affected-info .detail-table {
|
||||
display: block;
|
||||
}
|
||||
.notice-detail-incident-info .detail-table colgroup,
|
||||
.notice-detail-affected-info .detail-table colgroup {
|
||||
display: none;
|
||||
}
|
||||
.notice-detail-incident-info .detail-table tbody,
|
||||
.notice-detail-incident-info .detail-table tr,
|
||||
.notice-detail-incident-info .detail-table th,
|
||||
.notice-detail-incident-info .detail-table td,
|
||||
.notice-detail-affected-info .detail-table tbody,
|
||||
.notice-detail-affected-info .detail-table tr,
|
||||
.notice-detail-affected-info .detail-table th,
|
||||
.notice-detail-affected-info .detail-table td {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.notice-detail-incident-info .detail-table tr,
|
||||
.notice-detail-affected-info .detail-table tr {
|
||||
border-bottom: none;
|
||||
}
|
||||
.notice-detail-incident-info .detail-table th,
|
||||
.notice-detail-affected-info .detail-table th {
|
||||
text-align: left;
|
||||
background: #f8fafc;
|
||||
height: auto;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
.notice-detail-incident-info .detail-table td,
|
||||
.notice-detail-affected-info .detail-table td {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
.affected-apis-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.affected-apis-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.affected-api-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 16px;
|
||||
background-color: #f3f6fa;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
color: #555555;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
.affected-api-badge strong {
|
||||
color: #0a4ea3;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-affected-toggle {
|
||||
font-size: 13px;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
background-color: #0049b4;
|
||||
border: none;
|
||||
transition: all 0.2s ease;
|
||||
align-self: flex-end;
|
||||
margin-top: 8px;
|
||||
line-height: normal;
|
||||
}
|
||||
.btn-affected-toggle:hover {
|
||||
background-color: #003685;
|
||||
}
|
||||
.btn-affected-toggle .arrow {
|
||||
font-size: 10px;
|
||||
display: inline-block;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.btn-affected-toggle.active .arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.notice-detail-content {
|
||||
padding: 20px 0 60px 0;
|
||||
@@ -18507,35 +18605,8 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
}
|
||||
|
||||
.btn-notice-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 247px;
|
||||
height: 60px;
|
||||
background: #2a69de;
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.btn-notice-list {
|
||||
width: auto;
|
||||
min-width: 120px;
|
||||
padding: 0 20px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
.btn-notice-list:hover {
|
||||
background: rgb(31.8731707317, 92.7219512195, 205.7268292683);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.btn-notice-list:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.notice-detail-card {
|
||||
@@ -22637,12 +22708,39 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
}
|
||||
.service-sidebar {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.service-nav {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
flex-direction: row !important;
|
||||
flex-wrap: nowrap !important;
|
||||
justify-content: flex-start !important;
|
||||
padding: 16px 20px !important;
|
||||
overflow-x: auto !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
gap: 24px !important;
|
||||
border: 1px solid #e3e8f0 !important;
|
||||
border-radius: 20px !important;
|
||||
box-shadow: none !important;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
.service-nav::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.service-nav {
|
||||
-ms-overflow-style: none;
|
||||
/* IE and Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
}
|
||||
.service-nav .service-sidebar__profile,
|
||||
.service-nav .service-sidebar__divider {
|
||||
display: none !important;
|
||||
}
|
||||
.service-nav .service-nav__item {
|
||||
flex-shrink: 0 !important;
|
||||
white-space: nowrap !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.service-hero,
|
||||
.oauth2-2legged__hero {
|
||||
@@ -23448,6 +23546,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
background: #1A1A2E;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.oauth2-2legged__code-panel::before {
|
||||
content: "";
|
||||
|
||||
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
@@ -12,5 +12,5 @@ html {
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
.board-table {
|
||||
width: 100%;
|
||||
border-top: 1px solid #c4c7c8;
|
||||
|
||||
|
||||
&-wrapper {
|
||||
overflow-x: auto;
|
||||
margin-bottom: $spacing-xl;
|
||||
@@ -351,7 +351,7 @@
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
justify-content: flex-start;
|
||||
width: 100% !important;
|
||||
|
||||
|
||||
&::before {
|
||||
content: attr(data-label);
|
||||
font-weight: 500;
|
||||
@@ -364,9 +364,11 @@
|
||||
&--title {
|
||||
justify-content: flex-start;
|
||||
white-space: normal;
|
||||
|
||||
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
&::before { display: none; }
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-title-link {
|
||||
@@ -376,9 +378,9 @@
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// Mobile number prefix [1] [2] etc
|
||||
@@ -813,6 +815,7 @@
|
||||
// 공지사항 모바일 디자인 - 검색창, 목록, 페이지네이션
|
||||
// -----------------------------------------------------------------------------
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
|
||||
// 검색 필드 - Figma: 335px × 40px, border-radius 8px
|
||||
.search-field {
|
||||
display: flex;
|
||||
@@ -995,7 +998,8 @@
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
i, svg {
|
||||
i,
|
||||
svg {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
@@ -1019,4 +1023,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
--accent-orange: #FF6B6B;
|
||||
--accent-green: #6BCF7F;
|
||||
--accent-purple: #A78BFA;
|
||||
--accent-color:#efdcb2;
|
||||
--accent-color: #efdcb2;
|
||||
--accent-light: #E9F9FF;
|
||||
|
||||
// Base colors
|
||||
@@ -38,7 +38,7 @@
|
||||
--shadow-hover: 0 30px 60px -10px rgba(0, 73, 180, 0.2);
|
||||
|
||||
// border
|
||||
--border-color : #E2E8F0;
|
||||
--border-color: #E2E8F0;
|
||||
|
||||
//transition
|
||||
--transition-smooth: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
@@ -225,19 +225,19 @@ body.design-survey-active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size:24px;
|
||||
font-size: 24px;
|
||||
|
||||
.logo-text-bold {
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
letter-spacing: -0.5px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
|
||||
.logo-text-thin {
|
||||
font-weight: 300;
|
||||
color: var(--secondary-color);
|
||||
margin-left: 4px;
|
||||
font-weight: 300;
|
||||
color: var(--secondary-color);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -999,7 +999,7 @@ body.design-survey-active {
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
gap: 36px;
|
||||
gap: 28px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -1064,7 +1064,7 @@ body.design-survey-active {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--text-gray);
|
||||
padding: 8px 0;
|
||||
padding: 8px;
|
||||
position: relative;
|
||||
transition: var(--transition-smooth);
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
}
|
||||
|
||||
// Sidebar overrides for mobile toggle menu inside market
|
||||
// Commented out to allow horizontal scrollbar on mobile screens as well
|
||||
/*
|
||||
.service-sidebar {
|
||||
@media (max-width: 768px) {
|
||||
position: fixed;
|
||||
@@ -52,6 +54,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// Hero Section
|
||||
@@ -311,6 +314,7 @@
|
||||
flex: 1;
|
||||
// padding-left: $spacing-sm;
|
||||
// padding-right: $spacing-sm;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -334,37 +338,7 @@
|
||||
|
||||
// Mobile Menu Toggle
|
||||
.api-mobile-toggle {
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: $spacing-lg;
|
||||
right: $spacing-lg;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
|
||||
border-radius: $border-radius-circle;
|
||||
border: none;
|
||||
color: $white;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
box-shadow: $shadow-lg;
|
||||
z-index: $z-index-modal + 1;
|
||||
transition: $transition-base;
|
||||
background-color: $primary-blue;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: $shadow-xl;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// Header Section
|
||||
@@ -1097,7 +1071,11 @@
|
||||
|
||||
// API Market Content - Mobile (화면 폭 제한)
|
||||
.api-market-content {
|
||||
max-width: 100vw;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.api-card {
|
||||
height: 285px;
|
||||
}
|
||||
}
|
||||
@@ -158,6 +158,16 @@
|
||||
color: #0a4ea3;
|
||||
border: 1px solid #b3d4ff;
|
||||
}
|
||||
|
||||
|
||||
&.lg {
|
||||
height: 28px;
|
||||
width: auto;
|
||||
min-width: 50px;
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-detail-title {
|
||||
@@ -222,11 +232,13 @@
|
||||
.attachment-filename {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
// Incident Info Table
|
||||
.notice-detail-incident-info {
|
||||
.notice-detail-incident-info,
|
||||
.notice-detail-affected-info {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.detail-table {
|
||||
@@ -250,11 +262,112 @@
|
||||
}
|
||||
|
||||
td {
|
||||
padding-left: 19px;
|
||||
padding: 10px 19px;
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: block;
|
||||
|
||||
colgroup {
|
||||
display: none;
|
||||
}
|
||||
|
||||
tbody,
|
||||
tr,
|
||||
th,
|
||||
td {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
background: #f8fafc;
|
||||
height: auto;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Affected APIs Styling
|
||||
.affected-apis-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.affected-apis-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.affected-api-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 16px;
|
||||
background-color: #f3f6fa;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
color: #555555;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
|
||||
strong {
|
||||
color: #0a4ea3;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-affected-toggle {
|
||||
font-size: 13px;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
background-color: #0049b4;
|
||||
border: none;
|
||||
transition: all 0.2s ease;
|
||||
align-self: flex-end;
|
||||
margin-top: 8px;
|
||||
line-height: normal;
|
||||
|
||||
&:hover {
|
||||
background-color: #003685;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 10px;
|
||||
display: inline-block;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&.active .arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,37 +437,8 @@
|
||||
|
||||
// List Button - Figma Design
|
||||
.btn-notice-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 247px;
|
||||
height: 60px;
|
||||
background: #2a69de;
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: $transition-base;
|
||||
|
||||
@media (max-width: $breakpoint-md) {
|
||||
width: auto;
|
||||
min-width: 120px;
|
||||
padding: 0 20px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: darken(#2a69de, 10%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
// Notice detail card
|
||||
|
||||
@@ -227,13 +227,42 @@ $service-card-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgb
|
||||
|
||||
.service-sidebar {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.service-nav {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
flex-direction: row !important;
|
||||
flex-wrap: nowrap !important;
|
||||
justify-content: flex-start !important;
|
||||
padding: 16px 20px !important;
|
||||
overflow-x: auto !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
gap: 24px !important;
|
||||
border: 1px solid #e3e8f0 !important;
|
||||
border-radius: 20px !important;
|
||||
box-shadow: none !important;
|
||||
background: #ffffff !important;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
-ms-overflow-style: none;
|
||||
/* IE and Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
|
||||
.service-sidebar__profile,
|
||||
.service-sidebar__divider {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.service-nav__item {
|
||||
flex-shrink: 0 !important;
|
||||
white-space: nowrap !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 사이드바가 세로로 배치되면 가로 여백이 없으므로 100vw로 보정
|
||||
@@ -1244,6 +1273,7 @@ $o2leg-err-fg: #a23b3b;
|
||||
background: $o2leg-text-dark;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
margin-top: 24px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
|
||||
@@ -13,31 +13,24 @@
|
||||
</div>
|
||||
</th:block>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="api-market-container">
|
||||
<section class="container api-market-container">
|
||||
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="api-market-sidebar" id="apiSidebar">
|
||||
|
||||
<div class="api-sidebar-header">
|
||||
<img th:src="@{/img/api_sidebar.png}" alt="API">
|
||||
</div>
|
||||
|
||||
<nav class="api-sidebar-nav">
|
||||
<aside class="service-sidebar" id="apiSidebar">
|
||||
<nav class="service-nav">
|
||||
<!-- All APIs -->
|
||||
<div class="menu-section">
|
||||
<a class="menu-title" th:classappend="${selected == '-1'} ? 'active'" th:href="@{/apis}">
|
||||
전체
|
||||
</a>
|
||||
</div>
|
||||
<a class="service-nav__item"
|
||||
th:classappend="${selected == '-1' || #request.getParameter('selected') == '-1'} ? 'service-nav__item--active' : ''"
|
||||
th:href="@{/apis}">
|
||||
전체
|
||||
</a>
|
||||
|
||||
<!-- Service Categories -->
|
||||
<div class="menu-section" th:each="service : ${services}">
|
||||
<a class="menu-title"
|
||||
th:classappend="${selected == service.id} ? 'active'"
|
||||
th:href="@{/apis(groupIds=${service.id})}">
|
||||
[[${service.groupName}]]
|
||||
</a>
|
||||
</div>
|
||||
<a class="service-nav__item" th:each="service : ${services}"
|
||||
th:classappend="${(#request.getParameter('selected') != null ? #request.getParameter('selected') : (selected != null ? selected : apiSpecInfo.service)) == service.id} ? 'service-nav__item--active' : ''"
|
||||
th:href="@{/apis(groupIds=${service.id})}">
|
||||
[[${service.groupName}]]
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
<section class="service-hero">
|
||||
<div class="service-hero__inner">
|
||||
<div class="service-hero__icon-wrapper">
|
||||
<img th:src="@{/img/keyimage/api_img.svg}" alt="OPEN API 3D 아이콘"
|
||||
style="width: 100%; height: 100%; object-fit: contain;" />
|
||||
<img th:src="@{/img/keyimage/api_img.png}" alt="OPEN API 3D 아이콘" class="service-keyImg" />
|
||||
</div>
|
||||
<div class="service-hero__content">
|
||||
<div class="service-hero__badge">
|
||||
@@ -129,8 +128,11 @@
|
||||
apiCards.forEach(function (card) {
|
||||
// Click handler
|
||||
card.addEventListener('click', function () {
|
||||
const href = this.getAttribute('data-href');
|
||||
let href = this.getAttribute('data-href');
|
||||
if (href) {
|
||||
const selectedGroup = groupIdsInput ? groupIdsInput.value : '';
|
||||
const val = selectedGroup ? selectedGroup : '-1';
|
||||
href += (href.includes('?') ? '&' : '?') + 'selected=' + encodeURIComponent(val);
|
||||
window.location.href = href;
|
||||
}
|
||||
});
|
||||
@@ -139,8 +141,11 @@
|
||||
card.addEventListener('keypress', function (e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
const href = this.getAttribute('data-href');
|
||||
let href = this.getAttribute('data-href');
|
||||
if (href) {
|
||||
const selectedGroup = groupIdsInput ? groupIdsInput.value : '';
|
||||
const val = selectedGroup ? selectedGroup : '-1';
|
||||
href += (href.includes('?') ? '&' : '?') + 'selected=' + encodeURIComponent(val);
|
||||
window.location.href = href;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,116 +1,157 @@
|
||||
<!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}">
|
||||
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" alt="타이틀 배경">
|
||||
<h1>공지사항</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section layout:fragment="title">
|
||||
<div class="page-title-banner">
|
||||
<img th:src="@{/img/img_title_bg.png}" class="title-image" alt="타이틀 배경">
|
||||
<h1>공지사항</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<div class="signup-guide-v2">
|
||||
<!-- Hero Section -->
|
||||
<section class="service-hero">
|
||||
<div class="service-hero__inner">
|
||||
<div class="service-hero__icon-wrapper">
|
||||
<img th:src="@{/img/keyimage/notice_img.svg}" alt="공지사항 아이콘"
|
||||
style="width: 100%; height: 100%; object-fit: contain;" />
|
||||
</div>
|
||||
<div class="service-hero__content">
|
||||
<div class="service-hero__badge">
|
||||
<span class="service-hero__badge-dot"></span>
|
||||
<span class="service-hero__badge-text">고객지원</span>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<div class="signup-guide-v2">
|
||||
<!-- Hero Section -->
|
||||
<section class="service-hero">
|
||||
<div class="service-hero__inner">
|
||||
<div class="service-hero__icon-wrapper">
|
||||
<img th:src="@{/img/keyimage/notice_img.svg}" alt="공지사항 아이콘"
|
||||
style="width: 100%; height: 100%; object-fit: contain;" />
|
||||
</div>
|
||||
<h1 class="service-hero__title">공지사항</h1>
|
||||
<p class="service-hero__desc">DJBank 오픈 API 포털의 주요 안내 및 업데이트 소식을 전해드립니다.<br>원활한 서비스 연동을 위해 변경 사항을 주기적으로 확인해 주시기 바랍니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="service-main">
|
||||
<!-- Sidebar -->
|
||||
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('notice')}"></th:block>
|
||||
|
||||
<!-- Content -->
|
||||
<section class="service-content">
|
||||
<div class="notice-detail-container" style="padding: 0; min-height: auto; margin: 0; background: transparent; box-shadow: none;">
|
||||
|
||||
<!-- Notice Header: Title and Date -->
|
||||
<div class="notice-detail-header">
|
||||
<p class="notice-detail-date" th:text="${#temporals.format(portalNotice.createdDate, 'yyyy.MM.dd')}">2026.06.24</p>
|
||||
<div class="notice-detail-title-wrapper">
|
||||
<span class="notice-type-badge notice-type-badge--incident" th:if="${portalNotice.noticeType == '3'}">장애</span>
|
||||
<span class="notice-type-badge notice-type-badge--maintenance" th:if="${portalNotice.noticeType == '2'}">점검</span>
|
||||
<h2 class="notice-detail-title" th:text="${portalNotice.noticeSubject}">공지사항 디테일 입니다</h2>
|
||||
<div class="service-hero__content">
|
||||
<div class="service-hero__badge">
|
||||
<span class="service-hero__badge-dot"></span>
|
||||
<span class="service-hero__badge-text">고객지원</span>
|
||||
</div>
|
||||
<h1 class="service-hero__title">공지사항</h1>
|
||||
<p class="service-hero__desc">DJBank 오픈 API 포털의 주요 안내 및 업데이트 소식을 전해드립니다.<br>원활한 서비스 연동을 위해 변경 사항을 주기적으로 확인해
|
||||
주시기 바랍니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- 장애/점검 정보 영역 -->
|
||||
<div class="notice-detail-incident-info" th:if="${portalNotice.incidentOrMaintenance}">
|
||||
<table class="detail-table">
|
||||
<colgroup>
|
||||
<col style="width: 160px;"><col><col style="width: 160px;"><col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>시작</th>
|
||||
<td th:text="${portalNotice.startedAt != null ? #temporals.format(portalNotice.startedAt, 'yyyy-MM-dd HH:mm') : '-'}">-</td>
|
||||
<th>끝</th>
|
||||
<td th:text="${portalNotice.endAt != null ? #temporals.format(portalNotice.endAt, 'yyyy-MM-dd HH:mm') : '진행중'}">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>상태</th>
|
||||
<td th:text="${portalNotice.state != null ? portalNotice.state : '-'}">-</td>
|
||||
<th>영향 API</th>
|
||||
<td>
|
||||
<th:block th:if="${portalNotice.affectedApis != null and !portalNotice.affectedApis.isEmpty()}">
|
||||
<span th:each="api, iterStat : ${portalNotice.affectedApis}">
|
||||
<strong th:text="${api.apiId}">API_ID</strong><span th:if="${api.apiName != null and !api.apiName.isEmpty()}" th:text="| - ${api.apiName}|"></span><th:block th:if="${!iterStat.last}">, </th:block>
|
||||
</span>
|
||||
</th:block>
|
||||
<th:block th:unless="${portalNotice.affectedApis != null and !portalNotice.affectedApis.isEmpty()}">-</th:block>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Attachment Section -->
|
||||
<div class="notice-detail-attachment" th:if="${!#strings.isEmpty(portalNotice.fileId)}">
|
||||
<div class="attachment-list" th:with="fileInfo=${@fileService.findById(portalNotice.fileId)}">
|
||||
<div class="attachment-item" th:each="fileDetail, status : ${fileInfo.getFileDetails()}">
|
||||
<a th:href="'javascript:fn_downloadFile(\'' + ${fileDetail.fileId} + '\',\''+ ${fileDetail.fileSn} +'\')'" class="notice-attachment-link">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 15.75C18 17.4833 17.3917 18.9583 16.175 20.175C14.9583 21.3917 13.4833 22 11.75 22C10.0167 22 8.54167 21.3917 7.325 20.175C6.10833 18.9583 5.5 17.4833 5.5 15.75V6.5C5.5 5.25 5.9375 4.1875 6.8125 3.3125C7.6875 2.4375 8.75 2 10 2C11.25 2 12.3125 2.4375 13.1875 3.3125C14.0625 4.1875 14.5 5.25 14.5 6.5V15.25C14.5 16.0167 14.2333 16.6667 13.7 17.2C13.1667 17.7333 12.5167 18 11.75 18C10.9833 18 10.3333 17.7333 9.8 17.2C9.26667 16.6667 9 16.0167 9 15.25V6H11V15.25C11 15.4667 11.0708 15.6458 11.2125 15.7875C11.3542 15.9292 11.5333 16 11.75 16C11.9667 16 12.1458 15.9292 12.2875 15.7875C12.4292 15.6458 12.5 15.4667 12.5 15.25V6.5C12.4833 5.8 12.2375 5.20833 11.7625 4.725C11.2875 4.24167 10.7 4 10 4C9.3 4 8.70833 4.24167 8.225 4.725C7.74167 5.20833 7.5 5.8 7.5 6.5V15.75C7.48333 16.9333 7.89167 17.9375 8.725 18.7625C9.55833 19.5875 10.5667 20 11.75 20C12.9167 20 13.9083 19.5875 14.725 18.7625C15.5417 17.9375 15.9667 16.9333 16 15.75V6H18V15.75Z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span class="attachment-label">첨부파일</span>
|
||||
<span class="attachment-filename">[[${fileDetail.originalFileName}]].[[${fileDetail.fileExtension}]]</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Section -->
|
||||
<div class="notice-detail-content">
|
||||
<div id="noticeDetail" class="notice-content-body editor-content" th:utext="${portalNotice.noticeDetail}">
|
||||
공지사항 내용이 여기에 표시됩니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="notice-detail-actions">
|
||||
<button type="button" class="btn-notice-list" th:onclick="|location.href='@{/portalnotice}'|">목록</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container service-main">
|
||||
<!-- Sidebar -->
|
||||
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('notice')}"></th:block>
|
||||
|
||||
<!-- Content -->
|
||||
<section class="service-content">
|
||||
<div class="notice-detail-container"
|
||||
style="padding: 0; min-height: auto; margin: 0; background: transparent; box-shadow: none;">
|
||||
|
||||
<!-- Notice Header: Title and Date -->
|
||||
<div class="notice-detail-header">
|
||||
<p class="notice-detail-date" th:text="${#temporals.format(portalNotice.createdDate, 'yyyy.MM.dd')}">
|
||||
2026.06.24</p>
|
||||
<div class="notice-detail-title-wrapper">
|
||||
<span class="notice-type-badge lg notice-type-badge--incident"
|
||||
th:if="${portalNotice.noticeType == '3'}">장애</span>
|
||||
<span class="notice-type-badge lg notice-type-badge--maintenance"
|
||||
th:if="${portalNotice.noticeType == '2'}">점검</span>
|
||||
<h2 class="notice-detail-title" th:text="${portalNotice.noticeSubject}">공지사항 디테일 입니다</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 장애/점검 정보 영역 -->
|
||||
<div class="notice-detail-incident-info" th:if="${portalNotice.incidentOrMaintenance}">
|
||||
<table class="detail-table">
|
||||
<colgroup>
|
||||
<col style="width: 160px;">
|
||||
<col>
|
||||
<col style="width: 160px;">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>시작</th>
|
||||
<td
|
||||
th:text="${portalNotice.startedAt != null ? #temporals.format(portalNotice.startedAt, 'yyyy-MM-dd HH:mm') : '-'}">
|
||||
-</td>
|
||||
<th>끝</th>
|
||||
<td
|
||||
th:text="${portalNotice.endAt != null ? #temporals.format(portalNotice.endAt, 'yyyy-MM-dd HH:mm') : '진행중'}">
|
||||
-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>상태</th>
|
||||
<td colspan="3" th:text="${portalNotice.state != null ? portalNotice.state : '-'}">-</td>
|
||||
</tr>
|
||||
<tr th:if="${portalNotice.affectedApis != null and !portalNotice.affectedApis.isEmpty()}">
|
||||
<th>영향 API</th>
|
||||
<td colspan="3">
|
||||
<div class="affected-apis-wrapper">
|
||||
<!-- First 3 APIs (Always visible) -->
|
||||
<div class="affected-apis-list">
|
||||
<span class="affected-api-badge" th:each="api, iterStat : ${portalNotice.affectedApis}"
|
||||
th:if="${iterStat.index < 3}">
|
||||
<strong th:text="${api.apiId}">API_ID</strong><span
|
||||
th:if="${api.apiName != null and !api.apiName.isEmpty()}"
|
||||
th:text="| - ${api.apiName}|"></span>
|
||||
</span>
|
||||
|
||||
<!-- Rest of APIs (Hidden by default) -->
|
||||
<th:block th:if="${portalNotice.affectedApis.size() > 3}">
|
||||
<span class="affected-api-badge extra-api"
|
||||
th:each="api, iterStat : ${portalNotice.affectedApis}" th:if="${iterStat.index >= 3}"
|
||||
style="display: none;">
|
||||
<strong th:text="${api.apiId}">API_ID</strong><span
|
||||
th:if="${api.apiName != null and !api.apiName.isEmpty()}"
|
||||
th:text="| - ${api.apiName}|"></span>
|
||||
</span>
|
||||
</th:block>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Button -->
|
||||
<button type="button" class="btn-affected-toggle"
|
||||
th:if="${portalNotice.affectedApis.size() > 3}" onclick="toggleAffectedApis(this)">
|
||||
리스트 전체보기 <span class="arrow">▼</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Attachment Section -->
|
||||
<div class="notice-detail-attachment" th:if="${!#strings.isEmpty(portalNotice.fileId)}">
|
||||
<div class="attachment-list" th:with="fileInfo=${@fileService.findById(portalNotice.fileId)}">
|
||||
<div class="attachment-item" th:each="fileDetail, status : ${fileInfo.getFileDetails()}">
|
||||
<a th:href="'javascript:fn_downloadFile(\'' + ${fileDetail.fileId} + '\',\''+ ${fileDetail.fileSn} +'\')'"
|
||||
class="notice-attachment-link">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M18 15.75C18 17.4833 17.3917 18.9583 16.175 20.175C14.9583 21.3917 13.4833 22 11.75 22C10.0167 22 8.54167 21.3917 7.325 20.175C6.10833 18.9583 5.5 17.4833 5.5 15.75V6.5C5.5 5.25 5.9375 4.1875 6.8125 3.3125C7.6875 2.4375 8.75 2 10 2C11.25 2 12.3125 2.4375 13.1875 3.3125C14.0625 4.1875 14.5 5.25 14.5 6.5V15.25C14.5 16.0167 14.2333 16.6667 13.7 17.2C13.1667 17.7333 12.5167 18 11.75 18C10.9833 18 10.3333 17.7333 9.8 17.2C9.26667 16.6667 9 16.0167 9 15.25V6H11V15.25C11 15.4667 11.0708 15.6458 11.2125 15.7875C11.3542 15.9292 11.5333 16 11.75 16C11.9667 16 12.1458 15.9292 12.2875 15.7875C12.4292 15.6458 12.5 15.4667 12.5 15.25V6.5C12.4833 5.8 12.2375 5.20833 11.7625 4.725C11.2875 4.24167 10.7 4 10 4C9.3 4 8.70833 4.24167 8.225 4.725C7.74167 5.20833 7.5 5.8 7.5 6.5V15.75C7.48333 16.9333 7.89167 17.9375 8.725 18.7625C9.55833 19.5875 10.5667 20 11.75 20C12.9167 20 13.9083 19.5875 14.725 18.7625C15.5417 17.9375 15.9667 16.9333 16 15.75V6H18V15.75Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<span class="attachment-label">첨부파일</span>
|
||||
<span
|
||||
class="attachment-filename">[[${fileDetail.originalFileName}]].[[${fileDetail.fileExtension}]]</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Section -->
|
||||
<div class="notice-detail-content">
|
||||
<div id="noticeDetail" class="notice-content-body editor-content" th:utext="${portalNotice.noticeDetail}">
|
||||
공지사항 내용이 여기에 표시됩니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="notice-detail-actions">
|
||||
<button type="button" class="btn-action-primary btn-notice-list"
|
||||
th:onclick="|location.href='@{/portalnotice}'|">목록</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
</th:block>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -120,13 +161,31 @@
|
||||
window.open('[[@{/file/download}]]' + "?fileId=" + fileId + "&fileSn=" + fileSn);
|
||||
}
|
||||
|
||||
function toggleAffectedApis(btn) {
|
||||
const wrapper = btn.closest('.affected-apis-wrapper');
|
||||
const extraApis = wrapper.querySelectorAll('.extra-api');
|
||||
const isExpanded = btn.classList.contains('active');
|
||||
|
||||
extraApis.forEach(el => {
|
||||
el.style.display = isExpanded ? 'none' : 'inline-flex';
|
||||
});
|
||||
|
||||
if (isExpanded) {
|
||||
btn.classList.remove('active');
|
||||
btn.innerHTML = '리스트 전체보기 <span class="arrow">▼</span>';
|
||||
} else {
|
||||
btn.classList.add('active');
|
||||
btn.innerHTML = '리스트 접기 <span class="arrow">▲</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function decodeHTMLEntities(text) {
|
||||
var textArea = document.createElement('textarea');
|
||||
textArea.innerHTML = text;
|
||||
return textArea.value;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var element = document.getElementById('noticeDetail');
|
||||
if (element) {
|
||||
element.innerHTML = decodeHTMLEntities(element.innerHTML);
|
||||
@@ -135,4 +194,4 @@
|
||||
</script>
|
||||
</th:block>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -16,8 +16,7 @@
|
||||
<section class="service-hero">
|
||||
<div class="service-hero__inner">
|
||||
<div class="service-hero__icon-wrapper">
|
||||
<img th:src="@{/img/keyimage/notice_img.svg}" alt="공지사항 아이콘"
|
||||
style="width: 100%; height: 100%; object-fit: contain;" />
|
||||
<img th:src="@{/img/keyimage/notice_img.png}" alt="공지사항 아이콘" class="service-keyImg" />
|
||||
</div>
|
||||
<div class="service-hero__content">
|
||||
<div class="service-hero__badge">
|
||||
@@ -32,7 +31,7 @@
|
||||
</section>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="service-main">
|
||||
<div class="container service-main">
|
||||
<!-- Sidebar -->
|
||||
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('notice')}"></th:block>
|
||||
|
||||
@@ -86,8 +85,10 @@
|
||||
<span class="notice-number"
|
||||
th:text="|[${page.totalElements - (page.number * page.size) - status.index}]|">[1]</span>
|
||||
<span class="notice-type-badge notice-type-badge--fix" th:if="${notice.fixYn == 'Y'}">고정</span>
|
||||
<span class="notice-type-badge notice-type-badge--incident" th:if="${notice.noticeType == '3'}">장애</span>
|
||||
<span class="notice-type-badge notice-type-badge--maintenance" th:if="${notice.noticeType == '2'}">점검</span>
|
||||
<span class="notice-type-badge notice-type-badge--incident"
|
||||
th:if="${notice.noticeType == '3'}">장애</span>
|
||||
<span class="notice-type-badge notice-type-badge--maintenance"
|
||||
th:if="${notice.noticeType == '2'}">점검</span>
|
||||
<span th:text="${notice.noticeSubject}">공지사항 제목</span>
|
||||
<span class="file-icon" th:if="${notice.fileId != null and !notice.fileId.isEmpty()}">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
@@ -1,197 +1,224 @@
|
||||
<!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}">
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_title_layout}">
|
||||
|
||||
<body>
|
||||
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="oauth2-2legged">
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="oauth2-2legged">
|
||||
|
||||
<!-- Section 1: Hero -->
|
||||
<header class="oauth2-2legged__hero">
|
||||
<div class="oauth2-2legged__hero-inner">
|
||||
<div class="oauth2-2legged__hero-body">
|
||||
<span class="oauth2-2legged__hero-eyebrow">
|
||||
<span class="oauth2-2legged__hero-eyebrow-dot"></span>
|
||||
개발 가이드 · Webhook · HMAC-SHA256
|
||||
</span>
|
||||
<h1 class="oauth2-2legged__hero-title">웹훅 개발가이드</h1>
|
||||
<p class="oauth2-2legged__hero-lead">DJBank가 발송하는 Webhook 요청의 진위를 확인하기 위한</p>
|
||||
<p class="oauth2-2legged__hero-lead">HMAC-SHA256 서명 검증 방법을 단계별로 설명합니다.</p>
|
||||
<!-- Section 1: Hero -->
|
||||
<header class="oauth2-2legged__hero">
|
||||
<div class="oauth2-2legged__hero-inner">
|
||||
<div class="oauth2-2legged__hero-body">
|
||||
<span class="oauth2-2legged__hero-eyebrow">
|
||||
<span class="oauth2-2legged__hero-eyebrow-dot"></span>
|
||||
개발 가이드 · Webhook · HMAC-SHA256
|
||||
</span>
|
||||
<h1 class="oauth2-2legged__hero-title">웹훅 개발가이드</h1>
|
||||
<p class="oauth2-2legged__hero-lead">DJBank가 발송하는 Webhook 요청의 진위를 확인하기 위한 HMAC-SHA256 서명 검증 방법을
|
||||
단계별로 설명합니다.</p>
|
||||
|
||||
<div class="oauth2-2legged__hero-chips">
|
||||
<!-- <div class="oauth2-2legged__hero-chips">
|
||||
<span class="oauth2-2legged__chip oauth2-2legged__chip--primary">HMAC-SHA256</span>
|
||||
<span class="oauth2-2legged__chip">X-Webhook-Signature</span>
|
||||
<span class="oauth2-2legged__chip">Raw Body</span>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="oauth2-2legged__hero-illust" aria-hidden="true">
|
||||
<svg viewBox="0 0 280 200" xmlns="http://www.w3.org/2000/svg" role="img"
|
||||
aria-label="Signed Webhook Delivery">
|
||||
<defs>
|
||||
<marker id="whsig-hero-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8"
|
||||
markerHeight="8" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#0049b4" />
|
||||
</marker>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="280" height="200" rx="20" fill="#FFFFFF" />
|
||||
|
||||
<rect x="24" y="92" width="86" height="52" rx="8" fill="#EDF9FE" stroke="#0049b4" />
|
||||
<text x="67" y="114" text-anchor="middle" font-size="10" font-weight="700"
|
||||
fill="#0049b4">DJBank</text>
|
||||
<text x="67" y="128" text-anchor="middle" font-size="10" font-weight="700"
|
||||
fill="#0049b4">Webhook</text>
|
||||
|
||||
<rect x="170" y="92" width="86" height="52" rx="8" fill="#EDF9FE" stroke="#0049b4" />
|
||||
<text x="213" y="114" text-anchor="middle" font-size="10" font-weight="700"
|
||||
fill="#0049b4">Your</text>
|
||||
<text x="213" y="128" text-anchor="middle" font-size="10" font-weight="700"
|
||||
fill="#0049b4">Endpoint</text>
|
||||
|
||||
<line x1="110" y1="118" x2="170" y2="118" stroke="#0049b4" stroke-width="2"
|
||||
marker-end="url(#whsig-hero-arrow)" />
|
||||
|
||||
<g transform="translate(58,28)">
|
||||
<rect width="164" height="34" rx="6" fill="#1A1A2E" />
|
||||
<text x="12" y="16" font-family="'Fira Code', monospace" font-size="9"
|
||||
fill="#00D4FF">X-Webhook-Signature:</text>
|
||||
<text x="12" y="28" font-family="'Fira Code', monospace" font-size="9"
|
||||
fill="#FFD93D">sha256=9f86d0..</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(133,104)">
|
||||
<circle r="13" fill="#0049b4" />
|
||||
<path d="M-5,-1 h10 v7 h-10 z M-3,-1 v-3 a3,3 0 0 1 6,0 v3" fill="none" stroke="#FFFFFF"
|
||||
stroke-width="1.5" />
|
||||
</g>
|
||||
|
||||
<text x="140" y="172" text-anchor="middle" font-size="11" font-weight="600"
|
||||
fill="#64748B">Signed Webhook Delivery</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="oauth2-2legged__hero-illust" aria-hidden="true">
|
||||
<svg viewBox="0 0 280 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Signed Webhook Delivery">
|
||||
<defs>
|
||||
<marker id="whsig-hero-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#0049b4"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="280" height="200" rx="20" fill="#FFFFFF"/>
|
||||
<div class="container service-main">
|
||||
<!-- Sidebar -->
|
||||
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('webhookGuide')}"></th:block>
|
||||
|
||||
<rect x="24" y="92" width="86" height="52" rx="8" fill="#EDF9FE" stroke="#0049b4"/>
|
||||
<text x="67" y="114" text-anchor="middle" font-size="10" font-weight="700" fill="#0049b4">DJBank</text>
|
||||
<text x="67" y="128" text-anchor="middle" font-size="10" font-weight="700" fill="#0049b4">Webhook</text>
|
||||
<div class="service-content">
|
||||
|
||||
<rect x="170" y="92" width="86" height="52" rx="8" fill="#EDF9FE" stroke="#0049b4"/>
|
||||
<text x="213" y="114" text-anchor="middle" font-size="10" font-weight="700" fill="#0049b4">Your</text>
|
||||
<text x="213" y="128" text-anchor="middle" font-size="10" font-weight="700" fill="#0049b4">Endpoint</text>
|
||||
<!-- Section 2: 개요 -->
|
||||
<section class="oauth2-2legged__prereq" aria-labelledby="whsig-prereq-title">
|
||||
<span class="oauth2-2legged__eyebrow">OVERVIEW</span>
|
||||
<h2 class="oauth2-2legged__h2" id="whsig-prereq-title">서명 검증이 필요한 이유</h2>
|
||||
|
||||
<line x1="110" y1="118" x2="170" y2="118" stroke="#0049b4" stroke-width="2" marker-end="url(#whsig-hero-arrow)"/>
|
||||
<div class="oauth2-2legged__prereq-grid">
|
||||
<article class="oauth2-2legged__prereq-card">
|
||||
<span class="oauth2-2legged__prereq-num">1</span>
|
||||
<div class="oauth2-2legged__prereq-body">
|
||||
<h3 class="oauth2-2legged__prereq-title">Secret 확보</h3>
|
||||
<p>[마이페이지 > Webhook 관리]에서 발급된</p>
|
||||
<p>Secret Key를 서버에 안전하게 보관.</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<g transform="translate(58,28)">
|
||||
<rect width="164" height="34" rx="6" fill="#1A1A2E"/>
|
||||
<text x="12" y="16" font-family="'Fira Code', monospace" font-size="9" fill="#00D4FF">X-Webhook-Signature:</text>
|
||||
<text x="12" y="28" font-family="'Fira Code', monospace" font-size="9" fill="#FFD93D">sha256=9f86d0..</text>
|
||||
</g>
|
||||
<article class="oauth2-2legged__prereq-card">
|
||||
<span class="oauth2-2legged__prereq-num">2</span>
|
||||
<div class="oauth2-2legged__prereq-body">
|
||||
<h3 class="oauth2-2legged__prereq-title">원문(raw body) 보존</h3>
|
||||
<p>수신 즉시 본문을 파싱/재직렬화하지 말고</p>
|
||||
<p>바이트 원문 그대로 서명 계산에 사용.</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<g transform="translate(133,104)">
|
||||
<circle r="13" fill="#0049b4"/>
|
||||
<path d="M-5,-1 h10 v7 h-10 z M-3,-1 v-3 a3,3 0 0 1 6,0 v3" fill="none" stroke="#FFFFFF" stroke-width="1.5"/>
|
||||
</g>
|
||||
<article class="oauth2-2legged__prereq-card">
|
||||
<span class="oauth2-2legged__prereq-num">3</span>
|
||||
<div class="oauth2-2legged__prereq-body">
|
||||
<h3 class="oauth2-2legged__prereq-title">HMAC 재계산·비교</h3>
|
||||
<p>동일 Secret으로 HMAC-SHA256을 재계산해</p>
|
||||
<p>헤더 서명과 상수 시간으로 비교.</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<text x="140" y="172" text-anchor="middle" font-size="11" font-weight="600" fill="#64748B">Signed Webhook Delivery</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Section 3: 전체 서명·검증 시퀀스 -->
|
||||
<section class="oauth2-2legged__sequence" aria-labelledby="whsig-seq-title">
|
||||
<span class="oauth2-2legged__eyebrow">SEQUENCE</span>
|
||||
<h2 class="oauth2-2legged__h2" id="whsig-seq-title">전체 서명·검증 시퀀스</h2>
|
||||
|
||||
<div class="container service-main">
|
||||
<!-- Sidebar -->
|
||||
<th:block th:replace="~{fragment/djbank/service_sidebar :: sidebar('webhookGuide')}"></th:block>
|
||||
<div class="oauth2-2legged__sequence-diagram">
|
||||
<svg viewBox="0 0 1120 300" xmlns="http://www.w3.org/2000/svg" role="img"
|
||||
aria-label="Webhook Signature Verification Sequence">
|
||||
<defs>
|
||||
<marker id="whsig-arrow-primary" viewBox="0 0 10 10" refX="9" refY="5"
|
||||
markerWidth="8" markerHeight="8" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#0049b4" />
|
||||
</marker>
|
||||
<marker id="whsig-arrow-gray" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8"
|
||||
markerHeight="8" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#64748B" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<div class="service-content">
|
||||
<g>
|
||||
<rect x="120" y="24" width="240" height="48" rx="24" fill="#EDF9FE"
|
||||
stroke="#0049b4" />
|
||||
<text x="240" y="54" text-anchor="middle" font-size="14" font-weight="700"
|
||||
fill="#0049b4">DJBank Webhook Sender</text>
|
||||
<line x1="240" y1="72" x2="240" y2="272" stroke="#94A3B8" stroke-dasharray="4 4" />
|
||||
</g>
|
||||
<g>
|
||||
<rect x="760" y="24" width="240" height="48" rx="24" fill="#FFFFFF"
|
||||
stroke="#0049b4" />
|
||||
<text x="880" y="54" text-anchor="middle" font-size="14" font-weight="700"
|
||||
fill="#0049b4">Your Endpoint</text>
|
||||
<line x1="880" y1="72" x2="880" y2="272" stroke="#94A3B8" stroke-dasharray="4 4" />
|
||||
</g>
|
||||
|
||||
<!-- Section 2: 개요 -->
|
||||
<section class="oauth2-2legged__prereq" aria-labelledby="whsig-prereq-title">
|
||||
<span class="oauth2-2legged__eyebrow">OVERVIEW</span>
|
||||
<h2 class="oauth2-2legged__h2" id="whsig-prereq-title">서명 검증이 필요한 이유</h2>
|
||||
<text x="240" y="104" text-anchor="middle" font-size="12" font-weight="700"
|
||||
fill="#64748B">① 이벤트 발생 (점검·지연·장애)</text>
|
||||
<text x="240" y="124" text-anchor="middle" font-size="12" font-weight="700"
|
||||
fill="#64748B">② signature = HMAC-SHA256(secret, body)</text>
|
||||
|
||||
<div class="oauth2-2legged__prereq-grid">
|
||||
<article class="oauth2-2legged__prereq-card">
|
||||
<span class="oauth2-2legged__prereq-num">1</span>
|
||||
<div class="oauth2-2legged__prereq-body">
|
||||
<h3 class="oauth2-2legged__prereq-title">Secret 확보</h3>
|
||||
<p>[마이페이지 > Webhook 관리]에서 발급된</p>
|
||||
<p>Secret Key를 서버에 안전하게 보관.</p>
|
||||
</div>
|
||||
</article>
|
||||
<text x="560" y="156" text-anchor="middle" font-size="12" font-weight="700"
|
||||
fill="#0049b4">③ POST body · X-Webhook-Signature: sha256=<hex></text>
|
||||
<line x1="240" y1="166" x2="880" y2="166" stroke="#0049b4" stroke-width="2"
|
||||
marker-end="url(#whsig-arrow-primary)" />
|
||||
|
||||
<article class="oauth2-2legged__prereq-card">
|
||||
<span class="oauth2-2legged__prereq-num">2</span>
|
||||
<div class="oauth2-2legged__prereq-body">
|
||||
<h3 class="oauth2-2legged__prereq-title">원문(raw body) 보존</h3>
|
||||
<p>수신 즉시 본문을 파싱/재직렬화하지 말고</p>
|
||||
<p>바이트 원문 그대로 서명 계산에 사용.</p>
|
||||
</div>
|
||||
</article>
|
||||
<text x="880" y="198" text-anchor="middle" font-size="12" font-weight="700"
|
||||
fill="#64748B">④ 동일 secret으로 재계산</text>
|
||||
<text x="880" y="218" text-anchor="middle" font-size="12" font-weight="700"
|
||||
fill="#64748B">⑤ 상수 시간 비교 (일치 여부)</text>
|
||||
|
||||
<article class="oauth2-2legged__prereq-card">
|
||||
<span class="oauth2-2legged__prereq-num">3</span>
|
||||
<div class="oauth2-2legged__prereq-body">
|
||||
<h3 class="oauth2-2legged__prereq-title">HMAC 재계산·비교</h3>
|
||||
<p>동일 Secret으로 HMAC-SHA256을 재계산해</p>
|
||||
<p>헤더 서명과 상수 시간으로 비교.</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<text x="560" y="250" text-anchor="middle" font-size="12" font-weight="700"
|
||||
fill="#64748B">⑥ 200 OK (검증 성공 시)</text>
|
||||
<line x1="880" y1="260" x2="240" y2="260" stroke="#64748B" stroke-width="2"
|
||||
marker-end="url(#whsig-arrow-gray)" />
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 3: 전체 서명·검증 시퀀스 -->
|
||||
<section class="oauth2-2legged__sequence" aria-labelledby="whsig-seq-title">
|
||||
<span class="oauth2-2legged__eyebrow">SEQUENCE</span>
|
||||
<h2 class="oauth2-2legged__h2" id="whsig-seq-title">전체 서명·검증 시퀀스</h2>
|
||||
<!-- Section 4: STEP 1 — 수신 요청 형식 -->
|
||||
<section class="oauth2-2legged__step" aria-labelledby="whsig-step1-title">
|
||||
<span class="oauth2-2legged__eyebrow">STEP 1</span>
|
||||
<h2 class="oauth2-2legged__h2" id="whsig-step1-title">수신 요청 형식</h2>
|
||||
<p class="oauth2-2legged__desc">DJBank는 등록한 수신 URL로 아래 형태의 POST 요청을 전송합니다.</p>
|
||||
|
||||
<div class="oauth2-2legged__sequence-diagram">
|
||||
<svg viewBox="0 0 1120 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Webhook Signature Verification Sequence">
|
||||
<defs>
|
||||
<marker id="whsig-arrow-primary" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#0049b4"/>
|
||||
</marker>
|
||||
<marker id="whsig-arrow-gray" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#64748B"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<div class="oauth2-2legged__endpoint-box">
|
||||
<span class="oauth2-2legged__method">POST</span>
|
||||
<code class="oauth2-2legged__endpoint-path">https://your-service.example.com/webhook</code>
|
||||
<span class="oauth2-2legged__endpoint-content-type">application/json</span>
|
||||
</div>
|
||||
|
||||
<g>
|
||||
<rect x="120" y="24" width="240" height="48" rx="24" fill="#EDF9FE" stroke="#0049b4"/>
|
||||
<text x="240" y="54" text-anchor="middle" font-size="14" font-weight="700" fill="#0049b4">DJBank Webhook Sender</text>
|
||||
<line x1="240" y1="72" x2="240" y2="272" stroke="#94A3B8" stroke-dasharray="4 4"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="760" y="24" width="240" height="48" rx="24" fill="#FFFFFF" stroke="#0049b4"/>
|
||||
<text x="880" y="54" text-anchor="middle" font-size="14" font-weight="700" fill="#0049b4">Your Endpoint</text>
|
||||
<line x1="880" y1="72" x2="880" y2="272" stroke="#94A3B8" stroke-dasharray="4 4"/>
|
||||
</g>
|
||||
<div class="oauth2-2legged__step-grid">
|
||||
<div class="oauth2-2legged__panel">
|
||||
<h3 class="oauth2-2legged__panel-title">요청 헤더</h3>
|
||||
<table class="oauth2-2legged__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>HEADER</th>
|
||||
<th>설명</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>X-Webhook-Signature</code></td>
|
||||
<td><code>sha256=<hex></code> — 본문 HMAC-SHA256 서명(소문자 hex)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>X-Webhook-Event</code></td>
|
||||
<td>이벤트 코드 (예: CONTROL_START)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>X-Webhook-Timestamp</code></td>
|
||||
<td>발송 시각 (epoch millis) — 재전송 방어용</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Content-Type</code></td>
|
||||
<td>application/json</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="oauth2-2legged__warning">⚠ 서명 대상은 파싱 전 <strong>본문 원문(raw body)</strong> 입니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<text x="240" y="104" text-anchor="middle" font-size="12" font-weight="700" fill="#64748B">① 이벤트 발생 (점검·지연·장애)</text>
|
||||
<text x="240" y="124" text-anchor="middle" font-size="12" font-weight="700" fill="#64748B">② signature = HMAC-SHA256(secret, body)</text>
|
||||
|
||||
<text x="560" y="156" text-anchor="middle" font-size="12" font-weight="700" fill="#0049b4">③ POST body · X-Webhook-Signature: sha256=<hex></text>
|
||||
<line x1="240" y1="166" x2="880" y2="166" stroke="#0049b4" stroke-width="2" marker-end="url(#whsig-arrow-primary)"/>
|
||||
|
||||
<text x="880" y="198" text-anchor="middle" font-size="12" font-weight="700" fill="#64748B">④ 동일 secret으로 재계산</text>
|
||||
<text x="880" y="218" text-anchor="middle" font-size="12" font-weight="700" fill="#64748B">⑤ 상수 시간 비교 (일치 여부)</text>
|
||||
|
||||
<text x="560" y="250" text-anchor="middle" font-size="12" font-weight="700" fill="#64748B">⑥ 200 OK (검증 성공 시)</text>
|
||||
<line x1="880" y1="260" x2="240" y2="260" stroke="#64748B" stroke-width="2" marker-end="url(#whsig-arrow-gray)"/>
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 4: STEP 1 — 수신 요청 형식 -->
|
||||
<section class="oauth2-2legged__step" aria-labelledby="whsig-step1-title">
|
||||
<span class="oauth2-2legged__eyebrow">STEP 1</span>
|
||||
<h2 class="oauth2-2legged__h2" id="whsig-step1-title">수신 요청 형식</h2>
|
||||
<p class="oauth2-2legged__desc">DJBank는 등록한 수신 URL로 아래 형태의 POST 요청을 전송합니다.</p>
|
||||
|
||||
<div class="oauth2-2legged__endpoint-box">
|
||||
<span class="oauth2-2legged__method">POST</span>
|
||||
<code class="oauth2-2legged__endpoint-path">https://your-service.example.com/webhook</code>
|
||||
<span class="oauth2-2legged__endpoint-content-type">application/json</span>
|
||||
</div>
|
||||
|
||||
<div class="oauth2-2legged__step-grid">
|
||||
<div class="oauth2-2legged__panel">
|
||||
<h3 class="oauth2-2legged__panel-title">요청 헤더</h3>
|
||||
<table class="oauth2-2legged__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>HEADER</th>
|
||||
<th>설명</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>X-Webhook-Signature</code></td>
|
||||
<td><code>sha256=<hex></code> — 본문 HMAC-SHA256 서명(소문자 hex)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>X-Webhook-Event</code></td>
|
||||
<td>이벤트 코드 (예: CONTROL_START)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>X-Webhook-Timestamp</code></td>
|
||||
<td>발송 시각 (epoch millis) — 재전송 방어용</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Content-Type</code></td>
|
||||
<td>application/json</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="oauth2-2legged__warning">⚠ 서명 대상은 파싱 전 <strong>본문 원문(raw body)</strong> 입니다.</p>
|
||||
</div>
|
||||
|
||||
<div class="oauth2-2legged__code-panel">
|
||||
<span class="oauth2-2legged__code-tag">Request Body · JSON</span>
|
||||
<pre class="oauth2-2legged__code-block">{
|
||||
<div class="oauth2-2legged__code-panel">
|
||||
<span class="oauth2-2legged__code-tag">Request Body · JSON</span>
|
||||
<pre class="oauth2-2legged__code-block">{
|
||||
<span class="o2leg-c">"eventType"</span>: <span class="o2leg-y">"CONTROL_START"</span>,
|
||||
<span class="o2leg-c">"eventId"</span>: <span class="o2leg-y">"f47ac10b-58cc-4372-a567-0e02b2c3d479"</span>,
|
||||
<span class="o2leg-c">"timestamp"</span>: <span class="o2leg-p">1723600000000</span>,
|
||||
@@ -199,20 +226,20 @@
|
||||
}
|
||||
|
||||
<span class="o2leg-g"># eventId: 발송 건 고유 ID · data: 영향 API 목록</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 5: STEP 2 — 서명 검증 알고리즘 -->
|
||||
<section class="oauth2-2legged__step" aria-labelledby="whsig-step2-title">
|
||||
<span class="oauth2-2legged__eyebrow">STEP 2</span>
|
||||
<h2 class="oauth2-2legged__h2" id="whsig-step2-title">서명 검증 알고리즘</h2>
|
||||
<p class="oauth2-2legged__desc">수신한 본문 원문과 발급된 Secret으로 서명을 재계산해 헤더 값과 비교합니다.</p>
|
||||
<!-- Section 5: STEP 2 — 서명 검증 알고리즘 -->
|
||||
<section class="oauth2-2legged__step" aria-labelledby="whsig-step2-title">
|
||||
<span class="oauth2-2legged__eyebrow">STEP 2</span>
|
||||
<h2 class="oauth2-2legged__h2" id="whsig-step2-title">서명 검증 알고리즘</h2>
|
||||
<p class="oauth2-2legged__desc">수신한 본문 원문과 발급된 Secret으로 서명을 재계산해 헤더 값과 비교합니다.</p>
|
||||
|
||||
<div class="oauth2-2legged__step-grid">
|
||||
<div class="oauth2-2legged__code-panel">
|
||||
<span class="oauth2-2legged__code-tag">Verification Steps</span>
|
||||
<pre class="oauth2-2legged__code-block"><span class="o2leg-cm"># 1) 헤더에서 서명 추출</span>
|
||||
<div class="oauth2-2legged__step-grid">
|
||||
<div class="oauth2-2legged__code-panel">
|
||||
<span class="oauth2-2legged__code-tag">Verification Steps</span>
|
||||
<pre class="oauth2-2legged__code-block"><span class="o2leg-cm"># 1) 헤더에서 서명 추출</span>
|
||||
received = header[<span class="o2leg-c">"X-Webhook-Signature"</span>] <span class="o2leg-g"># "sha256=...."</span>
|
||||
|
||||
<span class="o2leg-cm"># 2) 본문 원문으로 HMAC-SHA256 재계산</span>
|
||||
@@ -221,36 +248,36 @@ expected = <span class="o2leg-y">"sha256="</span> + toHexLower(digest)
|
||||
|
||||
<span class="o2leg-cm"># 3) 상수 시간 비교</span>
|
||||
valid = constantTimeEquals(received, expected)</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oauth2-2legged__panel">
|
||||
<h3 class="oauth2-2legged__panel-title">검증 규칙</h3>
|
||||
<ul class="oauth2-2legged__tips">
|
||||
<li>알고리즘: <code>HmacSHA256</code>, 키: Secret(UTF-8 bytes)</li>
|
||||
<li>메시지: 수신 <strong>본문 원문</strong>(UTF-8 bytes)</li>
|
||||
<li>출력: <strong>소문자 hex</strong>, 헤더는 <code>sha256=</code> 접두 포함</li>
|
||||
<li>비교: 타이밍 공격 방지를 위해 <strong>상수 시간</strong> 비교</li>
|
||||
<li>불일치 시 요청을 폐기하고 2xx 이외로 응답</li>
|
||||
</ul>
|
||||
<h4 class="oauth2-2legged__panel-subtitle">재전송(replay) 방어</h4>
|
||||
<ul class="oauth2-2legged__tips">
|
||||
<li><code>X-Webhook-Timestamp</code>가 허용 오차(예: 5분) 밖이면 거부</li>
|
||||
<li>동일 <code>eventId</code> 중복 수신은 멱등 처리</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="oauth2-2legged__panel">
|
||||
<h3 class="oauth2-2legged__panel-title">검증 규칙</h3>
|
||||
<ul class="oauth2-2legged__tips">
|
||||
<li>알고리즘: <code>HmacSHA256</code>, 키: Secret(UTF-8 bytes)</li>
|
||||
<li>메시지: 수신 <strong>본문 원문</strong>(UTF-8 bytes)</li>
|
||||
<li>출력: <strong>소문자 hex</strong>, 헤더는 <code>sha256=</code> 접두 포함</li>
|
||||
<li>비교: 타이밍 공격 방지를 위해 <strong>상수 시간</strong> 비교</li>
|
||||
<li>불일치 시 요청을 폐기하고 2xx 이외로 응답</li>
|
||||
</ul>
|
||||
<h4 class="oauth2-2legged__panel-subtitle">재전송(replay) 방어</h4>
|
||||
<ul class="oauth2-2legged__tips">
|
||||
<li><code>X-Webhook-Timestamp</code>가 허용 오차(예: 5분) 밖이면 거부</li>
|
||||
<li>동일 <code>eventId</code> 중복 수신은 멱등 처리</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 6: STEP 3 — 언어별 예제 -->
|
||||
<section class="oauth2-2legged__step" aria-labelledby="whsig-step3-title">
|
||||
<span class="oauth2-2legged__eyebrow">STEP 3</span>
|
||||
<h2 class="oauth2-2legged__h2" id="whsig-step3-title">언어별 서명 검증 예제</h2>
|
||||
<p class="oauth2-2legged__desc">프레임워크에서 반드시 <strong>원문 바디</strong>에 접근할 수 있어야 합니다.</p>
|
||||
<!-- Section 6: STEP 3 — 언어별 예제 -->
|
||||
<section class="oauth2-2legged__step" aria-labelledby="whsig-step3-title">
|
||||
<span class="oauth2-2legged__eyebrow">STEP 3</span>
|
||||
<h2 class="oauth2-2legged__h2" id="whsig-step3-title">언어별 서명 검증 예제</h2>
|
||||
<p class="oauth2-2legged__desc">프레임워크에서 반드시 <strong>원문 바디</strong>에 접근할 수 있어야 합니다.</p>
|
||||
|
||||
<div class="oauth2-2legged__step-grid">
|
||||
<div class="oauth2-2legged__code-panel">
|
||||
<span class="oauth2-2legged__code-tag">Node.js (Express)</span>
|
||||
<pre class="oauth2-2legged__code-block"><span class="o2leg-p">const</span> crypto = <span class="o2leg-y">require</span>(<span class="o2leg-c">'crypto'</span>);
|
||||
<div class="oauth2-2legged__step-grid">
|
||||
<div class="oauth2-2legged__code-panel">
|
||||
<span class="oauth2-2legged__code-tag">Node.js (Express)</span>
|
||||
<pre class="oauth2-2legged__code-block"><span class="o2leg-p">const</span> crypto = <span class="o2leg-y">require</span>(<span class="o2leg-c">'crypto'</span>);
|
||||
|
||||
<span class="o2leg-cm">// rawBody: express.raw() 등으로 확보한 원문 Buffer</span>
|
||||
<span class="o2leg-p">function</span> <span class="o2leg-y">verify</span>(rawBody, header, secret) {
|
||||
@@ -263,11 +290,11 @@ valid = constantTimeEquals(received, expected)</pre>
|
||||
<span class="o2leg-p">return</span> a.length === b.length &&
|
||||
crypto.<span class="o2leg-y">timingSafeEqual</span>(a, b);
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oauth2-2legged__code-panel">
|
||||
<span class="oauth2-2legged__code-tag">Python (Flask)</span>
|
||||
<pre class="oauth2-2legged__code-block"><span class="o2leg-p">import</span> hmac, hashlib
|
||||
<div class="oauth2-2legged__code-panel">
|
||||
<span class="oauth2-2legged__code-tag">Python (Flask)</span>
|
||||
<pre class="oauth2-2legged__code-block"><span class="o2leg-p">import</span> hmac, hashlib
|
||||
|
||||
<span class="o2leg-cm"># raw_body: request.get_data() 로 확보한 bytes</span>
|
||||
<span class="o2leg-p">def</span> <span class="o2leg-y">verify</span>(raw_body, header, secret):
|
||||
@@ -278,13 +305,13 @@ valid = constantTimeEquals(received, expected)</pre>
|
||||
).<span class="o2leg-y">hexdigest</span>()
|
||||
expected = <span class="o2leg-c">'sha256='</span> + digest
|
||||
<span class="o2leg-p">return</span> hmac.<span class="o2leg-y">compare_digest</span>(expected, header)</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oauth2-2legged__step-grid">
|
||||
<div class="oauth2-2legged__code-panel">
|
||||
<span class="oauth2-2legged__code-tag">Java</span>
|
||||
<pre class="oauth2-2legged__code-block"><span class="o2leg-p">import</span> javax.crypto.Mac;
|
||||
<div class="oauth2-2legged__step-grid">
|
||||
<div class="oauth2-2legged__code-panel">
|
||||
<span class="oauth2-2legged__code-tag">Java</span>
|
||||
<pre class="oauth2-2legged__code-block"><span class="o2leg-p">import</span> javax.crypto.Mac;
|
||||
<span class="o2leg-p">import</span> javax.crypto.spec.SecretKeySpec;
|
||||
<span class="o2leg-p">import</span> java.nio.charset.StandardCharsets;
|
||||
<span class="o2leg-p">import</span> java.security.MessageDigest;
|
||||
@@ -301,84 +328,106 @@ valid = constantTimeEquals(received, expected)</pre>
|
||||
expected.<span class="o2leg-y">getBytes</span>(StandardCharsets.UTF_8),
|
||||
header.<span class="o2leg-y">getBytes</span>(StandardCharsets.UTF_8));
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oauth2-2legged__panel">
|
||||
<h3 class="oauth2-2legged__panel-title">이벤트 코드 (X-Webhook-Event)</h3>
|
||||
<table class="oauth2-2legged__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>CODE</th>
|
||||
<th>의미</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>CONTROL_START</code></td><td>점검 시작</td></tr>
|
||||
<tr><td><code>CONTROL_END</code></td><td>점검 종료</td></tr>
|
||||
<tr><td><code>DELAY_START</code></td><td>지연 시작</td></tr>
|
||||
<tr><td><code>DELAY_END</code></td><td>지연 종료</td></tr>
|
||||
<tr><td><code>ERROR_START</code></td><td>장애 시작</td></tr>
|
||||
<tr><td><code>ERROR_END</code></td><td>장애 종료</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="oauth2-2legged__panel">
|
||||
<h3 class="oauth2-2legged__panel-title">이벤트 코드 (X-Webhook-Event)</h3>
|
||||
<table class="oauth2-2legged__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>CODE</th>
|
||||
<th>의미</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>CONTROL_START</code></td>
|
||||
<td>점검 시작</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>CONTROL_END</code></td>
|
||||
<td>점검 종료</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DELAY_START</code></td>
|
||||
<td>지연 시작</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DELAY_END</code></td>
|
||||
<td>지연 종료</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ERROR_START</code></td>
|
||||
<td>장애 시작</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ERROR_END</code></td>
|
||||
<td>장애 종료</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 6.5: STEP 4 — 응답 반환 규칙 -->
|
||||
<section class="oauth2-2legged__step" aria-labelledby="whsig-step4-title">
|
||||
<span class="oauth2-2legged__eyebrow">STEP 4</span>
|
||||
<h2 class="oauth2-2legged__h2" id="whsig-step4-title">응답(리턴) 반환 규칙</h2>
|
||||
<p class="oauth2-2legged__desc">수신 서버가 반환하는 HTTP 상태 코드에 따라 DJBank의 성공 판정과 재시도가 결정됩니다.</p>
|
||||
<!-- Section 6.5: STEP 4 — 응답 반환 규칙 -->
|
||||
<section class="oauth2-2legged__step" aria-labelledby="whsig-step4-title">
|
||||
<span class="oauth2-2legged__eyebrow">STEP 4</span>
|
||||
<h2 class="oauth2-2legged__h2" id="whsig-step4-title">응답(리턴) 반환 규칙</h2>
|
||||
<p class="oauth2-2legged__desc">수신 서버가 반환하는 HTTP 상태 코드에 따라 DJBank의 성공 판정과 재시도가 결정됩니다.</p>
|
||||
|
||||
<div class="oauth2-2legged__step-grid">
|
||||
<div class="oauth2-2legged__panel">
|
||||
<h3 class="oauth2-2legged__panel-title">상태 코드별 처리</h3>
|
||||
<table class="oauth2-2legged__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>반환</th>
|
||||
<th>상황</th>
|
||||
<th>DJBank 처리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="oauth2-2legged__req-badge oauth2-2legged__req-badge--required">2xx</span></td>
|
||||
<td>검증 성공 + 정상 접수</td>
|
||||
<td><strong>발송 성공</strong> 기록. 재시도 없음</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>400</code></td>
|
||||
<td>필수 헤더 누락</td>
|
||||
<td>실패 기록 + 재시도</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>서명 불일치 / timestamp 만료</td>
|
||||
<td>실패 기록 + 재시도</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>5xx</code> · 타임아웃</td>
|
||||
<td>수신 서버 일시 장애</td>
|
||||
<td>실패 기록 + 재시도</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="oauth2-2legged__warning">⚠ 2xx 이외 응답과 네트워크 오류는 <strong>일정 간격을 두고 재시도</strong>됩니다(기본 3회). 재시도로 인한 중복 수신은 <code>eventId</code> 멱등 처리로 방어하세요.</p>
|
||||
<div class="oauth2-2legged__step-grid">
|
||||
<div class="oauth2-2legged__panel">
|
||||
<h3 class="oauth2-2legged__panel-title">상태 코드별 처리</h3>
|
||||
<table class="oauth2-2legged__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>반환</th>
|
||||
<th>상황</th>
|
||||
<th>DJBank 처리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span
|
||||
class="oauth2-2legged__req-badge oauth2-2legged__req-badge--required">2xx</span>
|
||||
</td>
|
||||
<td>검증 성공 + 정상 접수</td>
|
||||
<td><strong>발송 성공</strong> 기록. 재시도 없음</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>400</code></td>
|
||||
<td>필수 헤더 누락</td>
|
||||
<td>실패 기록 + 재시도</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>서명 불일치 / timestamp 만료</td>
|
||||
<td>실패 기록 + 재시도</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>5xx</code> · 타임아웃</td>
|
||||
<td>수신 서버 일시 장애</td>
|
||||
<td>실패 기록 + 재시도</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="oauth2-2legged__warning">⚠ 2xx 이외 응답과 네트워크 오류는 <strong>일정 간격을 두고
|
||||
재시도</strong>됩니다(기본 3회). 재시도로 인한 중복 수신은 <code>eventId</code> 멱등 처리로 방어하세요.</p>
|
||||
|
||||
<h4 class="oauth2-2legged__panel-subtitle">응답 가이드</h4>
|
||||
<ul class="oauth2-2legged__tips">
|
||||
<li>검증 통과 시 <strong>즉시 200 OK</strong> 반환 — 무거운 후속 처리는 비동기로 분리</li>
|
||||
<li>응답 본문 규격은 자유(발송 로그에 기록만 됨) — 간단한 JSON 권장</li>
|
||||
<li>서명 검증 실패는 <code>401</code>, 필수 헤더 누락은 <code>400</code> 반환 권장</li>
|
||||
<li>동일 <code>eventId</code> 재수신 시 재처리 없이 200 반환(멱등)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h4 class="oauth2-2legged__panel-subtitle">응답 가이드</h4>
|
||||
<ul class="oauth2-2legged__tips">
|
||||
<li>검증 통과 시 <strong>즉시 200 OK</strong> 반환 — 무거운 후속 처리는 비동기로 분리</li>
|
||||
<li>응답 본문 규격은 자유(발송 로그에 기록만 됨) — 간단한 JSON 권장</li>
|
||||
<li>서명 검증 실패는 <code>401</code>, 필수 헤더 누락은 <code>400</code> 반환 권장</li>
|
||||
<li>동일 <code>eventId</code> 재수신 시 재처리 없이 200 반환(멱등)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="oauth2-2legged__code-panel">
|
||||
<span class="oauth2-2legged__code-tag oauth2-2legged__code-tag--ok">200 OK · JSON (권장)</span>
|
||||
<pre class="oauth2-2legged__code-block"><span class="o2leg-cm"># 정상 접수</span>
|
||||
<div class="oauth2-2legged__code-panel">
|
||||
<span class="oauth2-2legged__code-tag oauth2-2legged__code-tag--ok">200 OK · JSON
|
||||
(권장)</span>
|
||||
<pre class="oauth2-2legged__code-block"><span class="o2leg-cm"># 정상 접수</span>
|
||||
HTTP/1.1 <span class="o2leg-g">200 OK</span>
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -400,81 +449,81 @@ HTTP/1.1 <span class="o2leg-g">400 Bad Request</span>
|
||||
<span class="o2leg-c">"result"</span>: <span class="o2leg-y">"ERROR"</span>,
|
||||
<span class="o2leg-c">"message"</span>: <span class="o2leg-y">"필수 헤더가 누락되었습니다."</span>
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 7: 검증 실패 대표 원인 -->
|
||||
<section class="oauth2-2legged__errors" aria-labelledby="whsig-errors-title">
|
||||
<span class="oauth2-2legged__eyebrow">TROUBLESHOOTING</span>
|
||||
<h2 class="oauth2-2legged__h2" id="whsig-errors-title">서명 불일치 대표 원인</h2>
|
||||
|
||||
<div class="oauth2-2legged__panel">
|
||||
<table class="oauth2-2legged__table oauth2-2legged__table--errors">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>원인</th>
|
||||
<th>증상</th>
|
||||
<th>해결 가이드</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>본문 재직렬화</td>
|
||||
<td>JSON 파싱 후 다시 문자열화한 값으로 서명 계산</td>
|
||||
<td>파싱 전 raw body(bytes)로 계산</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>hex 대소문자</td>
|
||||
<td>대문자 hex로 비교해 불일치</td>
|
||||
<td>소문자 hex 사용</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>접두어 처리</td>
|
||||
<td><code>sha256=</code> 접두 포함/제외 불일치</td>
|
||||
<td>양쪽 모두 접두 포함 후 비교</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>인코딩</td>
|
||||
<td>Secret/본문을 UTF-8 외로 인코딩</td>
|
||||
<td>키·메시지 모두 UTF-8 bytes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Secret 불일치</td>
|
||||
<td>재발급 후 이전 Secret 사용</td>
|
||||
<td>최신 Secret으로 교체</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>재전송</td>
|
||||
<td>동일 이벤트 중복 수신</td>
|
||||
<td>timestamp 검사 + eventId 멱등 처리</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 8: CTA -->
|
||||
<a class="oauth2-2legged__cta" th:href="@{/webhook}">
|
||||
<div class="oauth2-2legged__cta-body">
|
||||
<span class="oauth2-2legged__cta-eyebrow">MANAGE</span>
|
||||
<h2 class="oauth2-2legged__cta-title">Webhook 관리로 가기</h2>
|
||||
<p class="oauth2-2legged__cta-desc">수신 URL·Secret·구독 이벤트를 등록하고 관리하세요.</p>
|
||||
<span class="oauth2-2legged__cta-button">Webhook 관리 →</span>
|
||||
</div>
|
||||
<span class="oauth2-2legged__cta-deco oauth2-2legged__cta-deco--lg" aria-hidden="true"></span>
|
||||
<span class="oauth2-2legged__cta-deco oauth2-2legged__cta-deco--sm" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Section 7: 검증 실패 대표 원인 -->
|
||||
<section class="oauth2-2legged__errors" aria-labelledby="whsig-errors-title">
|
||||
<span class="oauth2-2legged__eyebrow">TROUBLESHOOTING</span>
|
||||
<h2 class="oauth2-2legged__h2" id="whsig-errors-title">서명 불일치 대표 원인</h2>
|
||||
|
||||
<div class="oauth2-2legged__panel">
|
||||
<table class="oauth2-2legged__table oauth2-2legged__table--errors">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>원인</th>
|
||||
<th>증상</th>
|
||||
<th>해결 가이드</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>본문 재직렬화</td>
|
||||
<td>JSON 파싱 후 다시 문자열화한 값으로 서명 계산</td>
|
||||
<td>파싱 전 raw body(bytes)로 계산</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>hex 대소문자</td>
|
||||
<td>대문자 hex로 비교해 불일치</td>
|
||||
<td>소문자 hex 사용</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>접두어 처리</td>
|
||||
<td><code>sha256=</code> 접두 포함/제외 불일치</td>
|
||||
<td>양쪽 모두 접두 포함 후 비교</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>인코딩</td>
|
||||
<td>Secret/본문을 UTF-8 외로 인코딩</td>
|
||||
<td>키·메시지 모두 UTF-8 bytes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Secret 불일치</td>
|
||||
<td>재발급 후 이전 Secret 사용</td>
|
||||
<td>최신 Secret으로 교체</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>재전송</td>
|
||||
<td>동일 이벤트 중복 수신</td>
|
||||
<td>timestamp 검사 + eventId 멱등 처리</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 8: CTA -->
|
||||
<a class="oauth2-2legged__cta" th:href="@{/webhook}">
|
||||
<div class="oauth2-2legged__cta-body">
|
||||
<span class="oauth2-2legged__cta-eyebrow">MANAGE</span>
|
||||
<h2 class="oauth2-2legged__cta-title">Webhook 관리로 가기</h2>
|
||||
<p class="oauth2-2legged__cta-desc">수신 URL·Secret·구독 이벤트를 등록하고 관리하세요.</p>
|
||||
<span class="oauth2-2legged__cta-button">Webhook 관리 →</span>
|
||||
</div>
|
||||
<span class="oauth2-2legged__cta-deco oauth2-2legged__cta-deco--lg" aria-hidden="true"></span>
|
||||
<span class="oauth2-2legged__cta-deco oauth2-2legged__cta-deco--sm" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</th:block>
|
||||
</th:block>
|
||||
|
||||
</body>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
</th:block>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
Reference in New Issue
Block a user