본인 인증 추가 기능 및 UI 개선:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled

- Step-up 비밀번호 확인 페이지 구현
- 기존 비밀번호 확인 로직 제거 및 반영 직전 2FA 추가
- 보호 경로 완화 정책(PASSWORD/TWO_FACTOR) 및 경로 관리 확장
This commit is contained in:
Rinjae
2026-07-28 11:04:24 +09:00
parent 6c13b6bc63
commit fedd3e4c50
25 changed files with 603 additions and 165 deletions
+3
View File
@@ -316,6 +316,9 @@ page:
api_detail:
name: "API 상세보기"
path: "/apis/detail"
api_testbed:
name: "테스트베드"
path: "/apis/detail/testbed"
community:
name: 고객지원
path: "#"
+79 -44
View File
@@ -1160,7 +1160,7 @@ hr {
transition: all 0.3s ease;
}
.mobile-drawer .drawer-welcome .btn-drawer-login:hover {
background: rgb(0, 65.7, 162);
background: rgb(0%, 25.7647058824%, 63.5294117647%);
}
.mobile-drawer .drawer-welcome.authenticated {
flex-direction: row;
@@ -2456,7 +2456,7 @@ hr {
color: #FFFFFF;
}
.btn-success:hover {
background: rgb(83.2897959184, 199.3102040816, 106.493877551);
background: rgb(32.662665066%, 78.1608643457%, 41.762304922%);
transform: translateY(-3px);
}
.btn-danger {
@@ -2464,7 +2464,7 @@ hr {
color: #FFFFFF;
}
.btn-danger:hover {
background: rgb(255, 70.8, 70.8);
background: rgb(100%, 27.7647058824%, 27.7647058824%);
transform: translateY(-3px);
}
.btn-ghost {
@@ -2730,7 +2730,7 @@ hr {
.action-btn-delete:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1);
background: rgb(255, 88.9, 88.9);
background: rgb(100%, 34.862745098%, 34.862745098%);
}
.action-btn-delete:active {
transform: translateY(0);
@@ -2848,7 +2848,7 @@ hr {
background: #a4d6ea;
}
.btn-input-action.btn-change:hover {
background: rgb(131.6625, 199.4303571429, 226.5375);
background: rgb(51.6323529412%, 78.2079831933%, 88.8382352941%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1);
}
@@ -2923,7 +2923,7 @@ hr {
border: none;
}
.btn-action-primary:hover {
background: rgb(31.8731707317, 92.7219512195, 205.7268292683);
background: rgb(12.4992826399%, 36.3615494978%, 80.6771879484%);
transform: translateY(-2px);
color: #fff;
}
@@ -2966,7 +2966,7 @@ hr {
}
.status-badge.status-processing {
background: rgba(255, 217, 61, 0.1);
color: rgb(221.2, 177.8721649485, 0);
color: rgb(86.7450980392%, 69.7537901759%, 0%);
}
.status-badge.status-failed {
background: rgba(255, 107, 107, 0.1);
@@ -3006,7 +3006,7 @@ hr {
}
.status-badge-header.status-processing {
background: rgba(255, 217, 61, 0.1);
color: rgb(221.2, 177.8721649485, 0);
color: rgb(86.7450980392%, 69.7537901759%, 0%);
}
.badge-sm {
@@ -4194,7 +4194,7 @@ select.form-control {
.file-upload-wrapper .file-remove-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1);
background: rgb(255, 88.9, 88.9);
background: rgb(100%, 34.862745098%, 34.862745098%);
}
.file-upload-wrapper .file-remove-btn:active {
transform: translateY(0);
@@ -4515,7 +4515,7 @@ select.form-control {
transition: all 0.3s ease;
}
.form-actions--with-withdrawal .withdrawal-link:hover {
background: rgb(210.2090909091, 232.6045454545, 242.9409090909);
background: rgb(82.4349376114%, 91.2174688057%, 95.2709447415%);
}
.form-actions--with-withdrawal .withdrawal-link img {
width: 22px;
@@ -4670,7 +4670,7 @@ select.form-control {
text-decoration: underline;
}
.notice-content-box a:hover {
color: rgb(0, 65.7, 162);
color: rgb(0%, 25.7647058824%, 63.5294117647%);
}
.form-row--content .form-label-wrapper {
@@ -5541,7 +5541,7 @@ select.form-control {
font-size: 16px;
}
.drawer-logout-btn:hover {
background: rgb(255, 70.8, 70.8);
background: rgb(100%, 27.7647058824%, 27.7647058824%);
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(75, 155, 255, 0.15);
}
@@ -6254,7 +6254,7 @@ select.form-control {
color: #64748b;
}
.list-table-btn--default:hover {
background-color: rgb(233.3571428571, 233.3571428571, 231.1928571429);
background-color: rgb(91.512605042%, 91.512605042%, 90.6638655462%);
}
.list-table-btn--primary {
background-color: #ecf0fa;
@@ -6262,7 +6262,7 @@ select.form-control {
color: #2a69de;
}
.list-table-btn--primary:hover {
background-color: rgb(216.7625, 224.8125, 244.9375);
background-color: rgb(85.0049019608%, 88.1617647059%, 96.0539215686%);
}
.list-table-btn--secondary {
background-color: #f5f5f4;
@@ -6270,7 +6270,7 @@ select.form-control {
color: #64748b;
}
.list-table-btn--secondary:hover {
background-color: rgb(233.3571428571, 233.3571428571, 231.1928571429);
background-color: rgb(91.512605042%, 91.512605042%, 90.6638655462%);
}
.list-table-btn--danger {
background-color: #fbe7e9;
@@ -6278,7 +6278,7 @@ select.form-control {
color: #bb1026;
}
.list-table-btn--danger:hover {
background-color: rgb(247.5571428571, 210.3428571429, 214.0642857143);
background-color: rgb(97.081232493%, 82.487394958%, 83.9467787115%);
}
.table-pagination {
@@ -6928,7 +6928,7 @@ select.form-control {
.alert.alert-error {
background: rgba(255, 107, 107, 0.1);
border: 1px solid rgba(255, 107, 107, 0.3);
color: rgb(255, 70.8, 70.8);
color: rgb(100%, 27.7647058824%, 27.7647058824%);
align-items: center;
}
.alert.alert-error svg {
@@ -6942,7 +6942,7 @@ select.form-control {
.alert.alert-success {
background: rgba(107, 207, 127, 0.1);
border: 1px solid rgba(107, 207, 127, 0.3);
color: rgb(61.5183673469, 189.6816326531, 87.1510204082);
color: rgb(24.12484994%, 74.3849539816%, 34.1768707483%);
}
.alert.alert-info {
background: rgba(0, 73, 180, 0.1);
@@ -11213,6 +11213,8 @@ body.index-page-body {
transition: all 0.3s ease;
position: relative;
bottom: -2px;
display: inline-block;
text-decoration: none;
}
.api-detail-tabs .tab-button:hover {
color: #0049b4;
@@ -11241,6 +11243,39 @@ body.index-page-body {
margin: 24px 0;
}
.testbed-auth-gate {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
padding: calc(32px * 2) 24px;
text-align: center;
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.testbed-auth-gate .testbed-auth-gate__icon {
font-size: 40px;
line-height: 1;
}
.testbed-auth-gate .testbed-auth-gate__title {
margin: 0;
font-size: 20px;
font-weight: 700;
color: #1A1A2E;
}
.testbed-auth-gate .testbed-auth-gate__desc {
margin: 0;
font-size: 16px;
color: #64748B;
}
.testbed-auth-gate .btn {
margin-top: 8px;
min-width: 160px;
}
.testbed-app-panel {
margin: 8px 0 0;
padding: 24px;
@@ -11835,10 +11870,10 @@ body.index-page-body {
line-height: 1;
}
.login-button:hover {
background: rgb(0, 69.35, 171);
background: rgb(0%, 27.1960784314%, 67.0588235294%);
}
.login-button:active {
background: rgb(0, 65.7, 162);
background: rgb(0%, 25.7647058824%, 63.5294117647%);
}
.login-button:disabled {
opacity: 0.6;
@@ -12363,11 +12398,11 @@ body.index-page-body {
}
.auth-request-button:hover,
.auth-verify-button:hover {
background: rgb(147.83125, 206.7151785714, 230.26875);
background: rgb(57.9730392157%, 81.0647759104%, 90.3014705882%);
}
.auth-request-button:active,
.auth-verify-button:active {
background: rgb(131.6625, 199.4303571429, 226.5375);
background: rgb(51.6323529412%, 78.2079831933%, 88.8382352941%);
}
.auth-request-button:disabled,
.auth-verify-button:disabled {
@@ -12416,20 +12451,20 @@ body.index-page-body {
background: #e5e7eb;
}
.account-recovery-card .form-actions .cancel-button:hover {
background: rgb(215.8869565217, 218.8956521739, 224.9130434783);
background: rgb(84.6615515772%, 85.8414322251%, 88.2011935209%);
}
.account-recovery-card .form-actions .cancel-button:active {
background: rgb(202.7739130435, 206.7913043478, 214.8260869565);
background: rgb(79.5191815857%, 81.094629156%, 84.2455242967%);
}
.account-recovery-card .form-actions .submit-button {
color: #FFFFFF;
background: #0049B4;
}
.account-recovery-card .form-actions .submit-button:hover {
background: rgb(0, 69.35, 171);
background: rgb(0%, 27.1960784314%, 67.0588235294%);
}
.account-recovery-card .form-actions .submit-button:active {
background: rgb(0, 65.7, 162);
background: rgb(0%, 25.7647058824%, 63.5294117647%);
}
.account-recovery-card .form-actions .submit-button:disabled {
opacity: 0.6;
@@ -12638,7 +12673,7 @@ body.index-page-body {
transition: color 0.3s ease;
}
.result-info-box .info-text .info-link:hover {
color: rgb(0, 65.7, 162);
color: rgb(0%, 25.7647058824%, 63.5294117647%);
}
@media (max-width: 576px) {
.result-info-box .info-text {
@@ -17702,7 +17737,7 @@ input[type=checkbox]:checked + .custom-checkbox {
transition: background 0.2s ease;
}
.btn-copy-action:hover {
background: rgb(131.6625, 199.4303571429, 226.5375);
background: rgb(51.6323529412%, 78.2079831933%, 88.8382352941%);
}
@media (max-width: 768px) {
.btn-copy-action {
@@ -17729,7 +17764,7 @@ input[type=checkbox]:checked + .custom-checkbox {
transition: background 0.2s ease;
}
.btn-view-secret:hover {
background: rgb(31.8897196262, 151.4130841121, 234.5102803738);
background: rgb(12.5057724024%, 59.377680044%, 91.9648158329%);
}
.btn-view-secret svg {
width: 20px;
@@ -17914,7 +17949,7 @@ input[type=checkbox]:checked + .custom-checkbox {
border-radius: 8px;
}
.btn-copy-action:hover {
background: rgb(131.6625, 199.4303571429, 226.5375);
background: rgb(51.6323529412%, 78.2079831933%, 88.8382352941%);
}
.btn-view-secret {
width: 100% !important;
@@ -17930,7 +17965,7 @@ input[type=checkbox]:checked + .custom-checkbox {
height: 16px;
}
.btn-view-secret:hover {
background: rgb(31.8897196262, 151.4130841121, 234.5102803738);
background: rgb(12.5057724024%, 59.377680044%, 91.9648158329%);
}
#revealedSecretBox {
width: 100%;
@@ -18203,7 +18238,7 @@ input[type=checkbox]:checked + .custom-checkbox {
flex-shrink: 0;
}
.detail-wrap .dt-btn-copy:hover {
background: rgb(189.6, 230.0857142857, 255);
background: rgb(74.3529411765%, 90.2296918768%, 100%);
}
.detail-wrap .dt-btn-copy svg {
color: #2a69de;
@@ -18354,7 +18389,7 @@ input[type=checkbox]:checked + .custom-checkbox {
transition: background 0.2s ease;
}
.detail-wrap .dt-btn-gray:hover {
background: rgb(170.6589473684, 183.4378947368, 193.6610526316);
background: rgb(66.9250773994%, 71.9364293086%, 75.9455108359%);
}
.detail-wrap .dt-btn-red {
width: 156px;
@@ -18372,7 +18407,7 @@ input[type=checkbox]:checked + .custom-checkbox {
transition: background 0.2s ease;
}
.detail-wrap .dt-btn-red:hover {
background: rgb(255, 70.0915337423, 64.24);
background: rgb(100%, 27.4868759774%, 25.1921568627%);
}
.detail-wrap .dt-btn-blue {
width: 156px;
@@ -19847,7 +19882,7 @@ input[type=checkbox]:checked + .custom-checkbox {
}
}
.btn-inquiry-list:hover {
background: rgb(215.8869565217, 218.8956521739, 224.9130434783);
background: rgb(84.6615515772%, 85.8414322251%, 88.2011935209%);
}
.btn-inquiry-list:active {
transform: scale(0.98);
@@ -19880,7 +19915,7 @@ input[type=checkbox]:checked + .custom-checkbox {
}
}
.btn-inquiry-edit:hover {
background: rgb(0, 69.35, 171);
background: rgb(0%, 27.1960784314%, 67.0588235294%);
}
.btn-inquiry-edit:active {
transform: scale(0.98);
@@ -19913,7 +19948,7 @@ input[type=checkbox]:checked + .custom-checkbox {
}
}
.btn-inquiry-delete:hover {
background: rgb(217.9841772152, 41.3658227848, 58.2873417722);
background: rgb(85.4839910648%, 16.2218912882%, 22.8577810871%);
}
.btn-inquiry-delete:active {
transform: scale(0.98);
@@ -19985,7 +20020,7 @@ input[type=checkbox]:checked + .custom-checkbox {
margin-left: 8px;
}
.file-upload-inline .btn-remove-file-inline:hover {
background: rgb(209.4151898734, 36.2848101266, 52.8721518987);
background: rgb(82.1236038719%, 14.2293373045%, 20.7341772152%);
}
.file-upload-inline .btn-remove-file-inline svg {
width: 12px;
@@ -20017,7 +20052,7 @@ input[type=checkbox]:checked + .custom-checkbox {
}
}
.file-upload-inline .btn-file-attach:hover {
background: rgb(37.3117757009, 153.9304672897, 235.0082242991);
background: rgb(14.6320689023%, 60.3648891332%, 92.1600879604%);
}
.file-upload-inline .btn-file-attach svg {
width: 22px;
@@ -20077,7 +20112,7 @@ input[type=checkbox]:checked + .custom-checkbox {
border: none;
}
.inquiry-form-container .form-actions .btn-secondary:hover {
background: rgb(215.8869565217, 218.8956521739, 224.9130434783);
background: rgb(84.6615515772%, 85.8414322251%, 88.2011935209%);
}
.inquiry-form-container .form-actions .btn-primary {
background: #0049b4;
@@ -20085,7 +20120,7 @@ input[type=checkbox]:checked + .custom-checkbox {
border: none;
}
.inquiry-form-container .form-actions .btn-primary:hover {
background: rgb(0, 69.35, 171);
background: rgb(0%, 27.1960784314%, 67.0588235294%);
}
.inquiry-form-container .file-upload-inline .file-input-display {
min-height: 50px;
@@ -20874,7 +20909,7 @@ input[type=checkbox]:checked + .custom-checkbox {
cursor: pointer;
}
.djb-board-write-container .form-actions .btn-submit:hover {
background-color: rgb(33.643902439, 97.8731707317, 217.156097561);
background-color: rgb(13.193687231%, 38.3816355811%, 85.1592539455%);
}
@media (max-width: 768px) {
.djb-board-write-container .form-actions .btn-submit {
@@ -21356,7 +21391,7 @@ input[type=checkbox]:checked + .custom-checkbox {
transition: all 0.3s ease;
}
.org-file-remove:hover {
background: rgb(255, 70.8, 70.8);
background: rgb(100%, 27.7647058824%, 27.7647058824%);
}
.org-file-notice {
@@ -22338,7 +22373,7 @@ input[type=checkbox]:checked + .custom-checkbox {
}
.status-indicator.status-active {
background-color: rgba(107, 207, 127, 0.1);
color: rgb(83.2897959184, 199.3102040816, 106.493877551);
color: rgb(32.662665066%, 78.1608643457%, 41.762304922%);
}
.status-indicator.status-active .status-dot {
background-color: #6BCF7F;
File diff suppressed because one or more lines are too long
@@ -645,6 +645,9 @@
transition: $transition-base;
position: relative;
bottom: -2px;
// 탭이 <a> 링크로 전환됨(별도 URL) — 링크 기본 스타일 제거
display: inline-block;
text-decoration: none;
&:hover {
color: $primary-blue;
@@ -680,6 +683,44 @@
}
}
// Testbed 미인증 안내 — UI는 노출하되 접근 시 로그인 요구
.testbed-auth-gate {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: $spacing-md;
padding: calc(#{$spacing-xl} * 2) $spacing-lg;
text-align: center;
background: $white;
border: 1px solid $border-gray;
border-radius: $border-radius-lg;
box-shadow: $shadow-sm;
.testbed-auth-gate__icon {
font-size: 40px;
line-height: 1;
}
.testbed-auth-gate__title {
margin: 0;
font-size: $font-size-lg;
font-weight: $font-weight-bold;
color: $text-dark;
}
.testbed-auth-gate__desc {
margin: 0;
font-size: $font-size-base;
color: $text-gray;
}
.btn {
margin-top: $spacing-sm;
min-width: 160px;
}
}
// Testbed 앱 선택 패널 (DJPGPT0001)
.testbed-app-panel {
margin: $spacing-sm 0 0;
@@ -55,14 +55,18 @@
<!-- API Content -->
<div class="api-detail-content">
<!-- Tab Navigation -->
<!-- Tab Navigation: API 정보 / 테스트베드는 각각 별도 URL(딥링크). 활성 탭은 서버에서 결정 -->
<div class="api-detail-tabs">
<button class="tab-button active" data-tab="api-info">API 정보</button>
<button class="tab-button" data-tab="testbed">테스트베드</button>
<a class="tab-button"
th:classappend="${activeTab == 'testbed'} ? '' : 'active'"
th:href="@{/apis/detail(id=${apiSpecInfo.apiId})}">API 정보</a>
<a class="tab-button"
th:classappend="${activeTab == 'testbed'} ? 'active' : ''"
th:href="@{/apis/detail/testbed(id=${apiSpecInfo.apiId})}">테스트베드</a>
</div>
<!-- Tab Content -->
<div class="tab-content active" id="api-info-tab">
<div class="tab-content" th:classappend="${activeTab == 'testbed'} ? '' : 'active'" id="api-info-tab">
<!-- API Overview Card (Merged with Additional Information) -->
<div class="api-overview-card">
<div class="org-section-header org-section-header--agreement">
@@ -141,26 +145,38 @@
</div>
<!-- Testbed Tab Content -->
<div class="tab-content" id="testbed-tab">
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/swagger-ui.css}"/>
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/index.css}"/>
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/djb-swagger-testbed.css}"/>
<div class="tab-content" th:classappend="${activeTab == 'testbed'} ? 'active' : ''" id="testbed-tab">
<!-- DJPGPT0001: 앱(인증키) 선택 → 인증 정보 자동 주입 -->
<div class="testbed-app-panel" id="appsWrap">
<div class="testbed-app-panel__head">
<span class="testbed-app-panel__title">앱 선택</span>
<span class="testbed-app-panel__desc">테스트할 앱(인증키)을 선택하면 인증 정보가 자동 입력됩니다.</span>
</div>
<div class="testbed-app-field">
<select id="apps">
<option value="">앱을 선택하세요</option>
</select>
</div>
<p class="testbed-app-notice" id="appsNotice" style="display:none;"></p>
<!-- 미인증: 테스트베드는 로그인 후 이용 가능. UI는 노출하되 접근 시 인증 요구 -->
<div class="testbed-auth-gate" th:unless="${authenticated}">
<div class="testbed-auth-gate__icon">🔒</div>
<p class="testbed-auth-gate__title">테스트베드는 로그인 후 이용 가능합니다.</p>
<p class="testbed-auth-gate__desc">로그인하면 본인 앱(인증키)으로 API를 직접 호출·테스트할 수 있습니다.</p>
<a class="btn btn-primary" th:href="@{/login(reason='auth')}">로그인하기</a>
</div>
<div id="swagger-ui"></div>
<!-- 인증: Swagger 테스트베드 -->
<th:block th:if="${authenticated}">
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/swagger-ui.css}"/>
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/index.css}"/>
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/djb-swagger-testbed.css}"/>
<!-- DJPGPT0001: 앱(인증키) 선택 → 인증 정보 자동 주입 -->
<div class="testbed-app-panel" id="appsWrap">
<div class="testbed-app-panel__head">
<span class="testbed-app-panel__title">앱 선택</span>
<span class="testbed-app-panel__desc">테스트할 앱(인증키)을 선택하면 인증 정보가 자동 입력됩니다.</span>
</div>
<div class="testbed-app-field">
<select id="apps">
<option value="">앱을 선택하세요</option>
</select>
</div>
<p class="testbed-app-notice" id="appsNotice" style="display:none;"></p>
</div>
<div id="swagger-ui"></div>
</th:block>
</div>
</div>
@@ -255,42 +271,13 @@
}
});
// Tab functionality
const tabButtons = document.querySelectorAll('.tab-button');
const tabContents = document.querySelectorAll('.tab-content');
tabButtons.forEach(function(button) {
button.addEventListener('click', function() {
const targetTab = this.getAttribute('data-tab');
// Remove active class from all buttons and contents
tabButtons.forEach(function(btn) {
btn.classList.remove('active');
});
tabContents.forEach(function(content) {
content.classList.remove('active');
});
// Add active class to clicked button and corresponding content
this.classList.add('active');
document.getElementById(targetTab + '-tab').classList.add('active');
});
});
// Swagger UI functionality for testbed tab
let swaggerInitialized = false;
// 탭(API 정보 / 테스트베드)은 각각 별도 URL의 <a> 링크로 전환된다.
// 활성 탭은 서버가 결정하고, 테스트베드 탭이 활성이면서 인증된 경우에만
// Swagger UI를 초기화한다(실제 초기화는 하단에서 실행 — const 선언 이후).
const currentApiId = /*[[${apiSpecInfo.apiId}]]*/ 'default';
// Initialize Swagger UI when testbed tab is clicked
tabButtons.forEach(function(button) {
button.addEventListener('click', function() {
const targetTab = this.getAttribute('data-tab');
if (targetTab === 'testbed' && !swaggerInitialized) {
initializeSwagger(currentApiId);
swaggerInitialized = true;
}
});
});
const activeTab = /*[[${activeTab}]]*/ 'api-info';
const isAuthenticated = /*[[${authenticated}]]*/ false;
const testbedActive = (activeTab === 'testbed' && isAuthenticated);
function initializeSwagger(apiId) {
if (!apiId || apiId === 'default') {
@@ -517,10 +504,14 @@
.catch(function (e) { console.error('앱 인증정보 주입 실패', e); });
}
if (appsSelect) {
appsSelect.addEventListener('change', function () { onAppSelected(this.value); });
// 테스트베드 탭 활성 + 인증 상태일 때만 Swagger UI/앱 인증 컨텍스트 초기화
if (testbedActive) {
if (appsSelect) {
appsSelect.addEventListener('change', function () { onAppSelected(this.value); });
}
initializeSwagger(currentApiId);
loadTestbedContext();
}
loadTestbedContext();
});
</script>
</th:block>
@@ -8,7 +8,7 @@
<section class="service-hero">
<div class="service-hero__inner">
<div class="service-hero__icon-wrapper">
<img th:src="@{/img/keyimage/api_img.png}" alt="OPEN API 3D 아이콘" class="service-keyImg" />
<img th:src="@{/img/keyimage/api_img.svg}" alt="OPEN API 3D 아이콘" class="service-keyImg" />
</div>
<div class="service-hero__content">
<div class="service-hero__badge">
@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/djbank_title_layout}">
<body>
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>본인 확인</h1>
</div>
</section>
<section layout:fragment="contentFragment">
<div class="service-main">
<div class="app-management-content">
<div class="password-change-wrapper">
<h2 class="page-outer-title">본인 확인</h2>
<form th:action="@{/auth/stepup/password}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" name="returnUrl" th:value="${returnUrl}"/>
<div class="register-form-container">
<div class="info-notice-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4685ef" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 16v-4"></path>
<path d="M12 8h.01"></path>
</svg>
<p>내 정보 보호를 위하여 현재 비밀번호를 다시 입력해 주세요</p>
</div>
<div class="form-row">
<div class="form-label-wrapper">
<span class="form-label-text">현재 비밀번호</span>
</div>
<div class="form-field-wrapper">
<input type="password" name="currentPassword" class="form-input"
placeholder="비밀번호 입력" required autofocus>
</div>
</div>
</div>
<div class="form-actions" style="justify-content: flex-end;">
<div class="right-buttons">
<button type="button" class="btn-cancel" th:onclick="|location.href='@{/}'|">취소</button>
<button type="submit" class="btn-apply btn-primary">확인</button>
</div>
</div>
</form>
</div>
</div>
</div>
<script th:if="${error}" th:inline="javascript">
$(document).ready(function () {
customPopups.showAlert([[${error}]]);
})
</script>
</section>
</body>
</html>
@@ -16,7 +16,14 @@
/* 안내는 상단에서부터 노출(세로 중앙정렬 X → 팝업 뒤에 가려지지 않게) */
.tfa-challenge {
padding: 40px 16px 48px; /* 상단 약간의 여백만 */
/* 상단 = 고정 헤더 높이(80px) + 여백 40px. 이 페이지는 .container 를 직접 쓰므로
.content-wrapper .main-content 의 헤더 오프셋을 못 받아 직접 보정. */
padding: 120px 16px 48px;
}
@media (max-width: 768px) {
.tfa-challenge {
padding-top: 100px; /* 모바일 헤더 최대 60px + 여백 40px */
}
}
.tfa-challenge-card {
text-align: center;
@@ -16,7 +16,7 @@
<section class="service-hero">
<div class="service-hero__inner">
<div class="service-hero__icon-wrapper">
<img th:src="@{/img/keyimage/notice_img.png}" alt="공지사항 아이콘" class="service-keyImg" />
<img th:src="@{/img/keyimage/notice_img.svg}" alt="공지사항 아이콘" class="service-keyImg" />
</div>
<div class="service-hero__content">
<div class="service-hero__badge">
@@ -15,7 +15,7 @@
<a class="nav-link" th:href="@{/my_company_info}">기업 정보 변경</a>
</li>
<li class="nav-item">
<a class="nav-link active" th:href="@{/password/verify}">비밀번호 변경</a>
<a class="nav-link active" th:href="@{/password/change}">비밀번호 변경</a>
</li>
</ul>
<div class="card mt-2">
@@ -79,6 +79,35 @@
customPopups.showAlert([[${error}]]);
})
</script>
<script th:inline="javascript">
// 반영 직전 2FA: twofaRequired 면 제출을 가로채 2FA 팝업 → 성공 시 실제 제출.
(function () {
var twofaRequired = /*[[${twofaRequired}]]*/ false;
if (!twofaRequired) return;
var form = document.getElementById('passwordChangeForm');
if (!form) return;
form.addEventListener('submit', function (e) {
e.preventDefault();
var np = document.getElementById('newPassword').value;
var cp = form.querySelector('input[name="confirmPassword"]').value;
if (!np || !cp) { customPopups.showAlert('새 비밀번호를 입력해 주세요.'); return; }
if (np !== cp) { customPopups.showAlert('새 비밀번호가 일치하지 않습니다.'); return; }
if (window.PasswordPolicy && !PasswordPolicy.isValid(np)) {
customPopups.showAlert('비밀번호 규칙을 확인해 주세요.'); return;
}
if (typeof TwoFactorAuth === 'undefined') { form.submit(); return; }
TwoFactorAuth.open({
purpose: '/password/change',
// form.submit() 은 submit 이벤트를 재발생시키지 않으므로 그대로 서버로 전송된다.
onSuccess: function () { form.submit(); },
onCancel: function () { /* 사용자 취소 — 유지 */ }
});
});
})();
</script>
</section>
</body>
</html>
@@ -87,7 +87,7 @@
<a th:href="@{/statistics/api}"><i class="fas fa-chart-bar"></i>이용 통계</a>
</li>
<li><a th:href="@{/mypage}"><i class="fas fa-user-circle"></i>내 정보 관리</a></li>
<li><a th:href="@{/password/verify}"><i class="fas fa-lock"></i>비밀번호 변경</a></li>
<li><a th:href="@{/password/change}"><i class="fas fa-lock"></i>비밀번호 변경</a></li>
</ul>
</div>
</div>
@@ -243,7 +243,7 @@
<li sec:authorize="hasRole('ROLE_WEBHOOK')"><a th:href="@{/webhook}">Webhook 관리</a></li>
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/statistics/api}">이용 통계</a></li>
<li><a th:href="@{/mypage}">내 정보 관리</a></li>
<li><a th:href="@{/password/verify}">비밀번호 변경</a></li>
<li><a th:href="@{/password/change}">비밀번호 변경</a></li>
</ul>
</li>
</ul>
@@ -73,7 +73,7 @@
th:classappend="${activeMenu == 'profile'} ? 'service-nav__item--active' : ''"
class="service-nav__item">내정보 관리</a>
<a th:href="@{/password/verify}"
<a th:href="@{/password/change}"
th:classappend="${activeMenu == 'password'} ? 'service-nav__item--active' : ''"
class="service-nav__item">비밀번호 변경</a>
</th:block>