2FA 팝업 UI 개선 및 기능 추가:

- 디자인 가이드 기반 레이아웃/스타일 재정의(SASS)
- 인증 흐름 개선: 타이머/프로그레스바/버튼 상태 업데이트(JS)
- 사용자 친화적 안내 메시지 및 입력 상태 검증 추가
This commit is contained in:
Rinjae
2026-07-28 09:50:45 +09:00
parent b5ffa69eba
commit a4316545dc
7 changed files with 707 additions and 206 deletions
+276 -69
View File
@@ -7565,123 +7565,330 @@ button.djb-comment-submit:disabled {
color: #888;
}
#tfaModal .modal-title {
.tfa-card {
position: relative;
width: 520px;
max-width: 100%;
padding: 0;
border: 1px solid #DCE2ED;
border-radius: 4px;
background: #fff;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
color: #0D0E11;
letter-spacing: -0.02em;
}
.tfa-close {
position: absolute;
top: 16px;
right: 16px;
width: 28px;
height: 28px;
padding: 0;
border: 0;
background: none;
color: #7F8A95;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background 0.12s ease, color 0.12s ease;
}
.tfa-close:hover {
background: #F4F5F9;
color: #0D0E11;
}
.tfa-head {
padding: 24px 32px;
padding-right: 60px;
border-bottom: 1px solid #DCE2ED;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.tfa-head .tfa-head-title {
font-size: 27px;
font-weight: 700;
line-height: 1.35;
color: #0D0E11;
}
.tfa-head .tfa-head-brand {
font-size: 14px;
font-weight: 500;
color: #7F8A95;
}
.tfa-body {
padding: 28px 32px 32px;
}
.tfa-step {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.tfa-subtitle {
text-align: center;
color: #64748B;
margin-bottom: 24px;
font-size: 14px;
.tfa-step .tfa-step-num {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 100px;
background: #BDC7CF;
color: #fff;
font-size: 11px;
font-weight: 500;
flex: none;
}
.tfa-step .tfa-step-num.is-active {
background: #4685EF;
}
.tfa-step .tfa-step-text {
font-size: 15px;
font-weight: 500;
color: #7F8A95;
}
.tfa-step .tfa-step-text.is-active {
color: #0D0E11;
}
.tfa-segment {
display: flex;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 20px;
margin-bottom: 28px;
}
.tfa-segment .tfa-seg-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 12px 8px;
border: 1px solid #CBD5E1;
border-radius: 8px;
background: #F8FAFC;
color: #475569;
font-size: 14px;
font-weight: 600;
padding: 14px 16px;
text-align: left;
cursor: pointer;
transition: all 0.15s ease;
border: 1px solid #DCE2ED;
border-radius: 4px;
background: #fff;
transition: border-color 0.12s ease, background 0.12s ease;
}
.tfa-segment .tfa-seg-btn .tfa-seg-label {
display: block;
font-size: 15px;
font-weight: 500;
color: #0D0E11;
}
.tfa-segment .tfa-seg-btn .tfa-seg-masked {
display: block;
font-size: 14px;
font-weight: 300;
color: #7F8A95;
margin-top: 2px;
}
.tfa-segment .tfa-seg-btn:hover {
border-color: #0049B4;
border-color: #4685EF;
}
.tfa-segment .tfa-seg-btn.is-active {
background: #0049B4;
border-color: #0049B4;
color: #FFFFFF;
border: 1.5px solid #4685EF;
background: #ECF0FA;
}
.tfa-segment .tfa-seg-btn.is-active .tfa-seg-masked {
color: rgba(255, 255, 255, 0.85);
}
.tfa-segment .tfa-seg-masked {
font-size: 12px;
font-weight: 400;
color: #94A3B8;
.tfa-segment .tfa-seg-btn.is-active .tfa-seg-label {
color: #2A69DE;
}
.tfa-btn-block {
.tfa-hint {
border: 1px solid #DCE2ED;
border-radius: 4px;
background: #F4F5F9;
padding: 16px;
font-size: 14px;
font-weight: 300;
line-height: 1.5;
color: #7F8A95;
margin-bottom: 20px;
}
.tfa-cta {
width: 100%;
margin-top: 8px;
height: 56px;
border: 0;
border-radius: 4px;
background: #4685EF;
color: #fff;
cursor: pointer;
font-size: 16px;
font-weight: 500;
letter-spacing: -0.02em;
transition: background 0.12s ease;
}
.tfa-cta:hover {
background: #2A69DE;
}
.tfa-cta:disabled {
background: #F4F5F9;
color: #BDC7CF;
cursor: default;
}
.tfa-code-row {
position: relative;
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.tfa-code-row .tfa-code-input-wrap {
flex: 1;
display: flex;
align-items: center;
height: 56px;
padding: 0 14px;
border: 1px solid #BDC7CF;
border-radius: 4px;
background: #fff;
transition: border-color 0.12s ease;
}
.tfa-code-row .tfa-code-input-wrap:focus-within {
border-color: #4685EF;
}
.tfa-code-row .tfa-code-input {
width: 100%;
padding: 14px 64px 14px 16px;
border: 1px solid #CBD5E1;
border-radius: 8px;
font-size: 15px;
letter-spacing: 2px;
}
.tfa-code-row .tfa-code-input:focus {
flex: 1;
min-width: 0;
border: 0;
outline: none;
border-color: #0049B4;
background: transparent;
font-size: 16px;
font-weight: 500;
letter-spacing: 0.02em;
color: #0D0E11;
}
.tfa-code-row .tfa-timer {
position: absolute;
right: 16px;
color: #E11D48;
.tfa-code-row .tfa-code-input::placeholder {
color: #BDC7CF;
font-weight: 400;
}
.tfa-code-row .tfa-resend {
flex: none;
height: 56px;
padding: 0 18px;
border: 1px solid #7F8A95;
border-radius: 4px;
background: #fff;
color: #0D0E11;
cursor: pointer;
font-size: 15px;
font-weight: 500;
white-space: nowrap;
transition: background 0.12s ease;
}
.tfa-code-row .tfa-resend:hover {
background: #F4F5F9;
}
.tfa-code-row .tfa-resend:disabled {
opacity: 0.5;
cursor: default;
}
.tfa-timer-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
border: 1px solid #DCE2ED;
border-radius: 4px;
background: #F4F5F9;
padding: 12px 14px;
margin-bottom: 12px;
}
.tfa-timer-row .tfa-timer-label {
font-size: 14px;
font-weight: 600;
font-weight: 300;
color: #7F8A95;
}
.tfa-timer-row .tfa-timer-clock {
display: flex;
align-items: baseline;
gap: 6px;
}
.tfa-timer-row .tfa-timer {
font-size: 20px;
font-weight: 500;
letter-spacing: 0.02em;
color: #4685EF;
font-variant-numeric: tabular-nums;
}
.tfa-timer-row .tfa-timer.is-warning {
color: #F4253C;
}
.tfa-timer-row .tfa-timer.is-expired {
color: #7F8A95;
}
.tfa-timer-row .tfa-timer-total {
font-size: 13px;
font-weight: 300;
color: #7F8A95;
}
.tfa-progress {
height: 3px;
background: #DCE2ED;
border-radius: 2px;
overflow: hidden;
margin-bottom: 12px;
}
.tfa-progress .tfa-progress-bar {
height: 100%;
width: 100%;
background: #4685EF;
transition: width 1s linear;
}
.tfa-progress .tfa-progress-bar.is-warning {
background: #F4253C;
}
.tfa-message {
min-height: 18px;
margin-top: 8px;
font-size: 13px;
color: #64748B;
min-height: 21px;
margin: 0 0 20px;
font-size: 14px;
font-weight: 300;
line-height: 1.5;
color: #7F8A95;
}
.tfa-message.is-error {
color: #E11D48;
color: #F4253C;
}
.tfa-test-notice {
margin-top: 8px;
margin: 0 0 16px;
padding: 10px 14px;
border: 1px dashed #F59E0B;
border-radius: 8px;
border-radius: 4px;
background: #FFFBEB;
color: #B45309;
font-size: 13px;
text-align: center;
}
.tfa-actions {
display: flex;
justify-content: flex-end;
margin: 8px 0;
}
.tfa-actions .tfa-resend {
background: none;
border: none;
color: #0049B4;
font-size: 13px;
cursor: pointer;
text-decoration: underline;
.tfa-foot {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #DFDFDF;
font-size: 14px;
font-weight: 300;
line-height: 1.5;
color: #7F8A95;
}
@media (max-width: 768px) {
.tfa-head {
padding: 20px 22px;
}
.tfa-head .tfa-head-title {
font-size: 23px;
}
.tfa-body {
padding: 22px 22px 26px;
}
.tfa-segment {
margin-bottom: 22px;
}
}
.hero-carousel-section {
position: relative;
width: 100%;
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
@@ -90,13 +90,15 @@
var seg = document.getElementById('tfaSegment');
seg.innerHTML = '';
self._maskedByType = {};
var channels = info.channels || [];
channels.forEach(function (ch, idx) {
self._maskedByType[ch.type] = ch.masked;
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'tfa-seg-btn' + (idx === 0 ? ' is-active' : '');
btn.setAttribute('data-channel', ch.type);
btn.innerHTML = (ch.type === 'EMAIL' ? '이메일 인증' : '전화번호 인증')
btn.innerHTML = '<span class="tfa-seg-label">' + (ch.type === 'EMAIL' ? '이메일' : '휴대폰 문자') + '</span>'
+ '<span class="tfa-seg-masked">' + ch.masked + '</span>';
btn.addEventListener('click', function () { self._selectChannel(ch.type); });
seg.appendChild(btn);
@@ -106,8 +108,11 @@
// 초기 상태: 발송 단계
document.getElementById('tfaSendStep').style.display = '';
document.getElementById('tfaVerifyStep').style.display = 'none';
document.getElementById('tfaMessage').textContent = '';
self._setMessage('', false);
document.getElementById('tfaCodeInput').value = '';
document.getElementById('tfaVerifyButton').disabled = true;
self._setStep2Active(false);
self._resetTimerUi();
var notice = document.getElementById('tfaTestNotice');
notice.style.display = 'none';
notice.textContent = '';
@@ -118,6 +123,11 @@
document.getElementById('tfaVerifyButton').onclick = function () { self._verify(); };
document.getElementById('tfaCloseButton').onclick = function () { self._cancel('CANCELLED'); };
document.getElementById('tfaBackdrop').onclick = function () { self._cancel('CANCELLED'); };
document.getElementById('tfaCodeInput').oninput = function () {
var v = (this.value || '').replace(/\D/g, '').slice(0, 6);
this.value = v;
document.getElementById('tfaVerifyButton').disabled = v.length < 6;
};
document.getElementById('tfaCodeInput').onkeydown = function (e) {
if (e.key === 'Enter') { self._verify(); }
};
@@ -157,8 +167,11 @@
// 발송 성공 → 검증 단계 노출 + 타이머
document.getElementById('tfaSendStep').style.display = 'none';
document.getElementById('tfaVerifyStep').style.display = '';
self._setMessage('', false);
self._setStep2Active(true);
var masked = self._maskedByType ? (self._maskedByType[self._channel] || '') : '';
self._setMessage((masked ? masked + ' 으로 ' : '') + '인증번호를 보냈습니다.', false);
document.getElementById('tfaCodeInput').value = '';
document.getElementById('tfaVerifyButton').disabled = true;
document.getElementById('tfaCodeInput').focus();
if (self._testNotice && res.testAuthNumber) {
var notice = document.getElementById('tfaTestNotice');
@@ -226,21 +239,60 @@
});
},
_fmtClock: function (n) {
var m = Math.floor(n / 60);
var s = n % 60;
return m + ':' + (s < 10 ? '0' + s : s);
},
_setStep2Active: function (active) {
var num = document.getElementById('tfaStepNum');
var text = document.getElementById('tfaStepText');
if (num) { num.classList.toggle('is-active', active); }
if (text) { text.classList.toggle('is-active', active); }
},
_resetTimerUi: function () {
var base = this._ttl || 180;
var clock = document.getElementById('tfaTimer');
var bar = document.getElementById('tfaProgressBar');
var label = document.getElementById('tfaTimerLabel');
var total = document.getElementById('tfaTimerTotal');
if (clock) { clock.className = 'tfa-timer'; clock.textContent = this._fmtClock(base); }
if (bar) { bar.className = 'tfa-progress-bar'; bar.style.width = '100%'; }
if (label) { label.textContent = '남은 인증 시간'; }
if (total) { total.textContent = '/ ' + this._fmtClock(base); }
},
_startTimer: function (seconds) {
var self = this;
self._stopTimer();
var total = seconds || self._ttl || 180;
var remaining = seconds;
var el = document.getElementById('tfaTimer');
var clock = document.getElementById('tfaTimer');
var bar = document.getElementById('tfaProgressBar');
var label = document.getElementById('tfaTimerLabel');
var totalEl = document.getElementById('tfaTimerTotal');
if (label) { label.textContent = '남은 인증 시간'; }
if (totalEl) { totalEl.textContent = '/ ' + self._fmtClock(total); }
function tick() {
if (remaining <= 0) {
self._stopTimer();
el.textContent = '00:00';
if (clock) { clock.textContent = '0:00'; clock.className = 'tfa-timer is-expired'; }
if (bar) { bar.style.width = '0%'; }
if (label) { label.textContent = '인증 시간이 만료되었습니다. 재전송해 주세요.'; }
self._cancel('TIMEOUT');
return;
}
var m = Math.floor(remaining / 60);
var s = remaining % 60;
el.textContent = (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
var warn = remaining <= 30;
if (clock) {
clock.textContent = self._fmtClock(remaining);
clock.className = 'tfa-timer' + (warn ? ' is-warning' : '');
}
if (bar) {
bar.style.width = Math.round((remaining / total) * 100) + '%';
bar.className = 'tfa-progress-bar' + (warn ? ' is-warning' : '');
}
remaining--;
}
tick();
@@ -2,133 +2,352 @@
@use '../abstracts/color-functions' as *;
@use '../abstracts/mixins' as *;
// 공통 2FA(추가 인증) 팝업. .modal / .modal-backdrop / .modal-dialog 는 _modals 재사용.
// 공통 2FA(추가 인증) 팝업 — 제주은행(DJBank) ERP뱅킹 Design Guide 적용.
// .modal / .modal-backdrop / .modal 은 _modals 의 show/hide·애니메이션을 재사용하고
// 카드 내부는 아래 토큰(surface_blue #4685EF · border_gray #DCE2ED · radius 4)으로 재정의한다.
#tfaModal {
.modal-title {
display: flex;
align-items: center;
gap: 8px;
}
}
// 디자인 토큰(가이드 전용, 전역 변수와 분리)
$tfa-blue: #4685EF;
$tfa-blue-dark: #2A69DE;
$tfa-blue-soft: #ECF0FA;
$tfa-blue-text: #2A69DE;
$tfa-ink: #0D0E11;
$tfa-muted: #7F8A95;
$tfa-border: #DCE2ED;
$tfa-border-2: #BDC7CF;
$tfa-surface: #F4F5F9;
$tfa-danger: #F4253C;
.tfa-subtitle {
text-align: center;
color: #64748B;
margin-bottom: 24px;
font-size: 14px;
}
.tfa-segment {
display: flex;
gap: 8px;
margin-bottom: 20px;
.tfa-seg-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 12px 8px;
border: 1px solid #CBD5E1;
border-radius: 8px;
background: #F8FAFC;
color: #475569;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
border-color: $primary-color;
}
&.is-active {
background: $primary-color;
border-color: $primary-color;
color: #FFFFFF;
.tfa-seg-masked {
color: rgba(255, 255, 255, 0.85);
}
}
}
.tfa-seg-masked {
font-size: 12px;
font-weight: 400;
color: #94A3B8;
}
}
.tfa-btn-block {
width: 100%;
margin-top: 8px;
}
.tfa-code-row {
// 카드 (modal-dialog 기본 padding/max-width/radius 를 덮어씀)
.tfa-card {
position: relative;
width: 520px;
max-width: 100%;
padding: 0;
border: 1px solid $tfa-border;
border-radius: 4px;
background: #fff;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
color: $tfa-ink;
letter-spacing: -0.02em;
}
// 닫기 (우상단, 은은한 회색)
.tfa-close {
position: absolute;
top: 16px;
right: 16px;
width: 28px;
height: 28px;
padding: 0;
border: 0;
background: none;
color: $tfa-muted;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background 0.12s ease, color 0.12s ease;
&:hover {
background: $tfa-surface;
color: $tfa-ink;
}
}
// 헤더
.tfa-head {
padding: 24px 32px;
padding-right: 60px; // 우상단 닫기(✕) 영역 확보 — 브랜드 텍스트와 겹치지 않게
border-bottom: 1px solid $tfa-border;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
.tfa-head-title {
font-size: 27px;
font-weight: 700;
line-height: 1.35;
color: $tfa-ink;
}
.tfa-head-brand {
font-size: 14px;
font-weight: 500;
color: $tfa-muted;
}
}
.tfa-body {
padding: 28px 32px 32px;
}
// 단계 라벨 (① 인증 수단 / ② 인증번호 입력)
.tfa-step {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
.tfa-step-num {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 100px;
background: $tfa-border-2;
color: #fff;
font-size: 11px;
font-weight: 500;
flex: none;
&.is-active { background: $tfa-blue; }
}
.tfa-step-text {
font-size: 15px;
font-weight: 500;
color: $tfa-muted;
&.is-active { color: $tfa-ink; }
}
}
// 채널 선택 (이메일 / 휴대폰) — 2열 그리드
.tfa-segment {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 28px;
.tfa-seg-btn {
padding: 14px 16px;
text-align: left;
cursor: pointer;
border: 1px solid $tfa-border;
border-radius: 4px;
background: #fff;
transition: border-color 0.12s ease, background 0.12s ease;
.tfa-seg-label {
display: block;
font-size: 15px;
font-weight: 500;
color: $tfa-ink;
}
.tfa-seg-masked {
display: block;
font-size: 14px;
font-weight: 300;
color: $tfa-muted;
margin-top: 2px;
}
&:hover { border-color: $tfa-blue; }
&.is-active {
border: 1.5px solid $tfa-blue;
background: $tfa-blue-soft;
.tfa-seg-label { color: $tfa-blue-text; }
}
}
}
// 안내 박스 (발송 전)
.tfa-hint {
border: 1px solid $tfa-border;
border-radius: 4px;
background: $tfa-surface;
padding: 16px;
font-size: 14px;
font-weight: 300;
line-height: 1.5;
color: $tfa-muted;
margin-bottom: 20px;
}
// 기본 CTA (인증번호 받기 / 인증 완료)
.tfa-cta {
width: 100%;
height: 56px;
border: 0;
border-radius: 4px;
background: $tfa-blue;
color: #fff;
cursor: pointer;
font-size: 16px;
font-weight: 500;
letter-spacing: -0.02em;
transition: background 0.12s ease;
&:hover { background: $tfa-blue-dark; }
&:disabled {
background: $tfa-surface;
color: $tfa-border-2;
cursor: default;
}
}
// 인증번호 입력 + 재전송
.tfa-code-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
.tfa-code-input-wrap {
flex: 1;
display: flex;
align-items: center;
height: 56px;
padding: 0 14px;
border: 1px solid $tfa-border-2;
border-radius: 4px;
background: #fff;
transition: border-color 0.12s ease;
&:focus-within { border-color: $tfa-blue; }
}
.tfa-code-input {
width: 100%;
padding: 14px 64px 14px 16px;
border: 1px solid #CBD5E1;
border-radius: 8px;
font-size: 15px;
letter-spacing: 2px;
flex: 1;
min-width: 0;
border: 0;
outline: none;
background: transparent;
font-size: 16px;
font-weight: 500;
letter-spacing: 0.02em;
color: $tfa-ink;
&:focus {
outline: none;
border-color: $primary-color;
&::placeholder {
color: $tfa-border-2;
font-weight: 400;
}
}
.tfa-resend {
flex: none;
height: 56px;
padding: 0 18px;
border: 1px solid $tfa-muted;
border-radius: 4px;
background: #fff;
color: $tfa-ink;
cursor: pointer;
font-size: 15px;
font-weight: 500;
white-space: nowrap;
transition: background 0.12s ease;
&:hover { background: $tfa-surface; }
&:disabled { opacity: 0.5; cursor: default; }
}
}
// 타이머 행
.tfa-timer-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
border: 1px solid $tfa-border;
border-radius: 4px;
background: $tfa-surface;
padding: 12px 14px;
margin-bottom: 12px;
.tfa-timer-label {
font-size: 14px;
font-weight: 300;
color: $tfa-muted;
}
.tfa-timer-clock {
display: flex;
align-items: baseline;
gap: 6px;
}
.tfa-timer {
position: absolute;
right: 16px;
color: #E11D48;
font-size: 14px;
font-weight: 600;
font-size: 20px;
font-weight: 500;
letter-spacing: 0.02em;
color: $tfa-blue;
font-variant-numeric: tabular-nums;
&.is-warning { color: $tfa-danger; }
&.is-expired { color: $tfa-muted; }
}
.tfa-timer-total {
font-size: 13px;
font-weight: 300;
color: $tfa-muted;
}
}
// 진행 바
.tfa-progress {
height: 3px;
background: $tfa-border;
border-radius: 2px;
overflow: hidden;
margin-bottom: 12px;
.tfa-progress-bar {
height: 100%;
width: 100%;
background: $tfa-blue;
transition: width 1s linear;
&.is-warning { background: $tfa-danger; }
}
}
// 발송 안내 / 오류 메시지
.tfa-message {
min-height: 18px;
margin-top: 8px;
font-size: 13px;
color: #64748B;
min-height: 21px;
margin: 0 0 20px;
font-size: 14px;
font-weight: 300;
line-height: 1.5;
color: $tfa-muted;
&.is-error {
color: #E11D48;
}
&.is-error { color: $tfa-danger; }
}
// 테스트 인증번호 노출(개발/스테이지)
.tfa-test-notice {
margin-top: 8px;
margin: 0 0 16px;
padding: 10px 14px;
border: 1px dashed #F59E0B;
border-radius: 8px;
border-radius: 4px;
background: #FFFBEB;
color: #B45309;
font-size: 13px;
text-align: center;
}
.tfa-actions {
display: flex;
justify-content: flex-end;
margin: 8px 0;
.tfa-resend {
background: none;
border: none;
color: $primary-color;
font-size: 13px;
cursor: pointer;
text-decoration: underline;
}
// 하단 안내
.tfa-foot {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #DFDFDF;
font-size: 14px;
font-weight: 300;
line-height: 1.5;
color: $tfa-muted;
}
@media (max-width: $breakpoint-sm) {
.tfa-head { padding: 20px 22px; }
.tfa-head .tfa-head-title { font-size: 23px; }
.tfa-body { padding: 22px 22px 26px; }
.tfa-segment { margin-bottom: 22px; }
}
@@ -3,49 +3,72 @@
<div class="modal-backdrop" id="tfaBackdrop"></div>
<div class="modal" id="tfaModal">
<div class="modal-dialog">
<div class="modal-dialog tfa-card">
<div class="modal-header">
<h3 class="modal-title" id="tfaTitle">
<span aria-hidden="true">🔒</span> 추가 인증
</h3>
<button type="button" class="modal-close" id="tfaCloseButton" aria-label="닫기">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<button type="button" class="tfa-close" id="tfaCloseButton" aria-label="닫기">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<!-- 헤더 -->
<div class="tfa-head">
<span class="tfa-head-title" id="tfaTitle">추가 인증</span>
<span class="tfa-head-brand">DJBank</span>
</div>
<div class="modal-body">
<p class="tfa-subtitle">제주은행(DJBank)에 오신걸 환영합니다</p>
<div class="tfa-body">
<!-- 채널 선택 세그먼트 (이메일/전화번호) -->
<div class="tfa-segment" id="tfaSegment" role="tablist">
<!-- JS 가 채널 버튼을 렌더링 -->
<!-- STEP 1 : 인증 수단 -->
<div class="tfa-step">
<span class="tfa-step-num is-active">1</span>
<span class="tfa-step-text is-active">인증 수단</span>
</div>
<!-- 채널 선택(이메일/휴대폰) — JS 가 렌더링 -->
<div class="tfa-segment" id="tfaSegment" role="tablist"></div>
<!-- STEP 2 : 인증번호 입력 -->
<div class="tfa-step">
<span class="tfa-step-num" id="tfaStepNum">2</span>
<span class="tfa-step-text" id="tfaStepText">인증번호 입력</span>
</div>
<!-- 인증번호 발송 전 -->
<div id="tfaSendStep">
<button type="button" class="btn btn-primary tfa-btn-block" id="tfaSendButton">인증번호 발송</button>
<div class="tfa-hint">아래 버튼을 누르면 선택하신 수단으로 6자리 인증번호를 보내드립니다.</div>
<button type="button" class="tfa-cta" id="tfaSendButton">인증번호 받기</button>
</div>
<!-- 인증번호 발송 후 -->
<div id="tfaVerifyStep" style="display: none;">
<div class="tfa-code-row">
<input type="text" id="tfaCodeInput" class="tfa-code-input" inputmode="numeric" maxlength="6"
autocomplete="one-time-code" placeholder="인증번호를 입력해주세요"/>
<span class="tfa-timer" id="tfaTimer">03:00</span>
<div class="tfa-code-input-wrap">
<input type="text" id="tfaCodeInput" class="tfa-code-input" inputmode="numeric" maxlength="6"
autocomplete="one-time-code" placeholder="인증번호 6자리"/>
</div>
<button type="button" class="tfa-resend" id="tfaResendButton">재전송</button>
</div>
<div class="tfa-message" id="tfaMessage"></div>
<div class="tfa-timer-row">
<span class="tfa-timer-label" id="tfaTimerLabel">남은 인증 시간</span>
<span class="tfa-timer-clock">
<span class="tfa-timer" id="tfaTimer">3:00</span>
<span class="tfa-timer-total" id="tfaTimerTotal">/ 3:00</span>
</span>
</div>
<div class="tfa-progress"><div class="tfa-progress-bar" id="tfaProgressBar"></div></div>
<p class="tfa-message" id="tfaMessage"></p>
<div class="tfa-test-notice" id="tfaTestNotice" style="display: none;"></div>
<div class="tfa-actions">
<button type="button" class="tfa-resend" id="tfaResendButton">인증번호 재발송</button>
</div>
<button type="button" class="btn btn-primary tfa-btn-block" id="tfaVerifyButton">인증코드 확인</button>
<button type="button" class="tfa-cta" id="tfaVerifyButton" disabled>인증 완료</button>
</div>
<div class="tfa-foot">인증번호가 오지 않으면 스팸함을 확인하시거나 다른 수단으로 다시 시도해 주세요.</div>
</div>
</div>
</div>