fix : open api , 공지사항 반응형 퍼블리싱

This commit is contained in:
hong
2026-07-24 10:19:18 +09:00
parent 41391c8df0
commit a361cc3dec
15 changed files with 979 additions and 677 deletions
+177 -78
View File
@@ -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,8 +10227,10 @@ body.index-page-body {
padding: 20px;
}
}
@media (max-width: 768px) {
.api-market-container .service-sidebar {
.api-market-container {
/*
.service-sidebar {
@media (max-width: 768px) {
position: fixed;
left: 0;
top: 0;
@@ -10239,12 +10242,13 @@ body.index-page-body {
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 {
padding: 80px 24px 30px; // Clear header height if any
&.mobile-open {
transform: translateX(0);
}
.api-market-container .service-sidebar .service-nav {
.service-nav {
flex-direction: column !important;
gap: 16px !important;
border: none !important;
@@ -10252,6 +10256,9 @@ body.index-page-body {
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
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;
}
@@ -366,7 +366,9 @@
white-space: normal;
@media (max-width: $breakpoint-sm) {
&::before { display: none; }
&::before {
display: none;
}
}
.notice-title-link {
@@ -378,7 +380,7 @@
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;
@@ -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,7 +225,7 @@ body.design-survey-active {
display: flex;
align-items: center;
gap: 12px;
font-size:24px;
font-size: 24px;
.logo-text-bold {
font-weight: 700;
@@ -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;
}
}
+115 -31
View File
@@ -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 class="service-nav__item"
th:classappend="${selected == '-1' || #request.getParameter('selected') == '-1'} ? 'service-nav__item--active' : ''"
th:href="@{/apis}">
전체
</a>
</div>
<!-- Service Categories -->
<div class="menu-section" th:each="service : ${services}">
<a class="menu-title"
th:classappend="${selected == service.id} ? 'active'"
<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>
</div>
</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;
}
}
@@ -3,14 +3,14 @@
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/djbank_title_layout}">
<body>
<section layout:fragment="title">
<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>
<th:block layout:fragment="contentFragment">
<th:block layout:fragment="contentFragment">
<div class="signup-guide-v2">
<!-- Hero Section -->
<section class="service-hero">
@@ -25,26 +25,31 @@
<span class="service-hero__badge-text">고객지원</span>
</div>
<h1 class="service-hero__title">공지사항</h1>
<p class="service-hero__desc">DJBank 오픈 API 포털의 주요 안내 및 업데이트 소식을 전해드립니다.<br>원활한 서비스 연동을 위해 변경 사항을 주기적으로 확인해 주시기 바랍니다.</p>
<p class="service-hero__desc">DJBank 오픈 API 포털의 주요 안내 및 업데이트 소식을 전해드립니다.<br>원활한 서비스 연동을 위해 변경 사항을 주기적으로 확인해
주시기 바랍니다.</p>
</div>
</div>
</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>
<!-- Content -->
<section class="service-content">
<div class="notice-detail-container" style="padding: 0; min-height: auto; margin: 0; background: transparent; box-shadow: none;">
<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>
<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>
<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>
@@ -53,26 +58,57 @@
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<th:block th:unless="${portalNotice.affectedApis != null and !portalNotice.affectedApis.isEmpty()}">-</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>
@@ -83,12 +119,16 @@
<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">
<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"/>
<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>
<span
class="attachment-filename">[[${fileDetail.originalFileName}]].[[${fileDetail.fileExtension}]]</span>
</a>
</div>
</div>
@@ -103,14 +143,15 @@
<!-- Action Buttons -->
<div class="notice-detail-actions">
<button type="button" class="btn-notice-list" th:onclick="|location.href='@{/portalnotice}'|">목록</button>
<button type="button" class="btn-action-primary btn-notice-list"
th:onclick="|location.href='@{/portalnotice}'|">목록</button>
</div>
</div>
</section>
</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);
@@ -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">
@@ -4,7 +4,7 @@
<body>
<th:block layout:fragment="contentFragment">
<th:block layout:fragment="contentFragment">
<section class="oauth2-2legged">
<!-- Section 1: Hero -->
@@ -16,47 +16,58 @@
개발 가이드 · 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>
<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>
<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">
<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 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="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="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>
<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)"/>
<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>
<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"/>
<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>
<text x="140" y="172" text-anchor="middle" font-size="11" font-weight="600"
fill="#64748B">Signed Webhook Delivery</text>
</svg>
</div>
</div>
@@ -109,38 +120,53 @@
<h2 class="oauth2-2legged__h2" id="whsig-seq-title">전체 서명·검증 시퀀스</h2>
<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">
<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 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 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>
<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"/>
<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"/>
<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>
<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="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=&lt;hex&gt;</text>
<line x1="240" y1="166" x2="880" y2="166" stroke="#0049b4" stroke-width="2" marker-end="url(#whsig-arrow-primary)"/>
<text x="560" y="156" text-anchor="middle" font-size="12" font-weight="700"
fill="#0049b4">③ POST body · X-Webhook-Signature: sha256=&lt;hex&gt;</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="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)"/>
<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>
@@ -186,12 +212,13 @@
</tr>
</tbody>
</table>
<p class="oauth2-2legged__warning">⚠ 서명 대상은 파싱 전 <strong>본문 원문(raw body)</strong> 입니다.</p>
<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">{
<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>,
@@ -212,7 +239,7 @@
<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>
<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>
@@ -250,7 +277,7 @@ valid = constantTimeEquals(received, expected)</pre>
<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>);
<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) {
@@ -267,7 +294,7 @@ valid = constantTimeEquals(received, expected)</pre>
<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
<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):
@@ -284,7 +311,7 @@ valid = constantTimeEquals(received, expected)</pre>
<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;
<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;
@@ -313,12 +340,30 @@ valid = constantTimeEquals(received, expected)</pre>
</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>
<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>
@@ -344,7 +389,9 @@ valid = constantTimeEquals(received, expected)</pre>
</thead>
<tbody>
<tr>
<td><span class="oauth2-2legged__req-badge oauth2-2legged__req-badge--required">2xx</span></td>
<td><span
class="oauth2-2legged__req-badge oauth2-2legged__req-badge--required">2xx</span>
</td>
<td>검증 성공 + 정상 접수</td>
<td><strong>발송 성공</strong> 기록. 재시도 없음</td>
</tr>
@@ -365,7 +412,8 @@ valid = constantTimeEquals(received, expected)</pre>
</tr>
</tbody>
</table>
<p class="oauth2-2legged__warning">⚠ 2xx 이외 응답과 네트워크 오류는 <strong>일정 간격을 두고 재시도</strong>됩니다(기본 3회). 재시도로 인한 중복 수신은 <code>eventId</code> 멱등 처리로 방어하세요.</p>
<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">
@@ -377,8 +425,9 @@ valid = constantTimeEquals(received, expected)</pre>
</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>
<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
@@ -470,7 +519,7 @@ HTTP/1.1 <span class="o2leg-g">400 Bad Request</span>
</div>
</section>
</th:block>
</th:block>
</body>