- 회원가입 이메일 인증 페이지 추가 및 이메일 인증 흐름 구현
- 휴대폰 번호 중복 검증 로직 추가 및 프로퍼티 기반 설정 활성화 - 알림 수신 동의 항목 및 뷰 로직 추가
This commit is contained in:
@@ -317,6 +317,7 @@
|
||||
type: 'POST',
|
||||
data: {
|
||||
mobileNumber: mobileNumber,
|
||||
purpose: 'signup',
|
||||
_csrf: $('input[name="_csrf"]').val()
|
||||
},
|
||||
success: function(response) {
|
||||
|
||||
@@ -237,6 +237,14 @@
|
||||
form.appendChild(input);
|
||||
}
|
||||
|
||||
if (document.getElementById('notificationConsent')) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'notificationConsent';
|
||||
input.value = document.getElementById('notificationConsent').checked;
|
||||
form.appendChild(input);
|
||||
}
|
||||
|
||||
// 시스템 필드 추가
|
||||
const csrfToken = document.querySelector('input[name="_csrf"]');
|
||||
if (csrfToken) {
|
||||
@@ -338,9 +346,11 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
const notificationConsentEl = document.getElementById('notificationConsent');
|
||||
const validations = {
|
||||
termsOfUse: $('#termsOfUse').prop('checked'),
|
||||
privacyPolicy: $('#privacyCollect').prop('checked'),
|
||||
notificationConsent: !notificationConsentEl || notificationConsentEl.checked,
|
||||
compRegNo: $('#compRegNo').val().trim() !== '',
|
||||
corpRegNo: $('#corpRegNo').val().trim() !== '',
|
||||
orgName: $('#orgName').val().trim() !== '',
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
<!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="org-register-container">
|
||||
|
||||
<div class="common-title-bar">
|
||||
<h2 class="common-title">이메일 인증</h2>
|
||||
</div>
|
||||
|
||||
<div class="register-form-container">
|
||||
<p class="form-hint" style="margin-bottom: 16px;">
|
||||
회원가입을 완료하려면 가입하신 이메일 주소로 인증을 진행해 주세요.
|
||||
</p>
|
||||
<form id="signupVerificationEmailForm" role="form" name="signupVerificationEmailForm" method="post">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
|
||||
|
||||
<div class="register-form">
|
||||
<!-- 이메일 주소 + 인증코드 받기 버튼 (한 줄) -->
|
||||
<div class="form-row" style="margin-bottom: 0">
|
||||
<div class="form-label-wrapper label-offset">
|
||||
<span class="form-label-text">이메일 주소</span>
|
||||
</div>
|
||||
<div class="form-field-wrapper input-with-button">
|
||||
<input type="email" id="email" name="email" class="form-input input-readonly"
|
||||
th:value="${email}" readonly disabled="disabled">
|
||||
<button type="button" class="btn-input-action btn-auth btn_send_code">인증코드 받기</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row" style="margin-bottom: 0">
|
||||
<div class="form-label-wrapper label-offset">
|
||||
<span class="form-label-text"> </span>
|
||||
</div>
|
||||
<div class="form-field-wrapper">
|
||||
<p class="form-hint" style="margin-top: 0;">위 이메일 주소로 인증코드가 발송됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 인증번호 입력 -->
|
||||
<div class="form-row" id="authCodeContainer" style="display: none;">
|
||||
<div class="form-label-wrapper label-offset">
|
||||
<span class="form-label-text">인증코드 입력</span> <span class="required-badge">필수</span>
|
||||
</div>
|
||||
<div class="form-field-wrapper input-with-button">
|
||||
<div class="auth-input-group">
|
||||
<input type="text" id="authCode" name="authCode" maxlength="6" pattern="[0-9]*"
|
||||
inputmode="numeric" class="form-input"
|
||||
placeholder="이메일로 받은 인증코드 6자리">
|
||||
<span class="auth-timer" id="certify_time">05:00</span>
|
||||
</div>
|
||||
<button type="button" class="btn-input-action btn-auth btn_verify_code">인증코드 확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn-submit btn-secondary btn_cancel">나중에 인증</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
$(document).ready(function() {
|
||||
let timerInterval;
|
||||
let resendTimerInterval;
|
||||
let isVerified = false;
|
||||
|
||||
// CSRF 토큰 설정
|
||||
const csrfHeaderName = /*[[${_csrf.headerName}]]*/ 'X-XSRF-TOKEN';
|
||||
const csrfToken = /*[[${_csrf.token}]]*/ '';
|
||||
|
||||
// 서버 설정값
|
||||
const resendLimitSeconds = /*[[${@environment.getProperty('portal.auth.resend_limit_seconds', '60')}]]*/ 60;
|
||||
|
||||
// "나중에 인증" 클릭 시: 가입은 완료된 상태이므로 로그인 페이지로 이동 (로그인 후 인증 유도)
|
||||
$('.btn_cancel').on('click', function() {
|
||||
customPopups.showConfirm(
|
||||
'이메일 인증을 나중에 하시겠습니까? 로그인 후에도 인증을 완료할 수 있습니다.',
|
||||
function() {
|
||||
location.href = /*[[@{/login}]]*/ '/login';
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// 타이머 시작 함수 (인증 코드 유효 시간)
|
||||
function startTimer(duration) {
|
||||
let timer = duration;
|
||||
clearInterval(timerInterval);
|
||||
|
||||
timerInterval = setInterval(function () {
|
||||
let minutes = parseInt(timer / 60, 10);
|
||||
let seconds = parseInt(timer % 60, 10);
|
||||
|
||||
minutes = minutes < 10 ? "0" + minutes : minutes;
|
||||
seconds = seconds < 10 ? "0" + seconds : seconds;
|
||||
|
||||
$('#certify_time').text(minutes + ":" + seconds).css('color', '#666');
|
||||
|
||||
if (--timer < 0) {
|
||||
clearInterval(timerInterval);
|
||||
$('#certify_time').text("시간 초과").css('color', '#FF6B6B');
|
||||
customPopups.showAlert("인증 시간이 만료되었습니다. 인증코드를 다시 받아주세요.");
|
||||
$('.btn_send_code').prop('disabled', false).text('인증코드 재발송');
|
||||
$('.btn_verify_code').prop('disabled', true);
|
||||
$('#authCode').prop('readonly', true);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 재발송 타이머 시작 함수
|
||||
function startResendTimer() {
|
||||
let resendTimer = resendLimitSeconds;
|
||||
const $button = $('.btn_send_code');
|
||||
|
||||
clearInterval(resendTimerInterval);
|
||||
$button.prop('disabled', true);
|
||||
|
||||
resendTimerInterval = setInterval(function () {
|
||||
if (resendTimer > 0) {
|
||||
$button.text('재발송 가능 (' + resendTimer + '초)');
|
||||
resendTimer--;
|
||||
} else {
|
||||
clearInterval(resendTimerInterval);
|
||||
$button.prop('disabled', false).text('인증코드 재발송');
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 테스트 환경 안내 메시지 표시
|
||||
function displayTestAuthNotice(authNumber) {
|
||||
const noticeHtml = `
|
||||
<div class="test-env-notice" style="margin-top: 12px; padding: 12px 16px; background-color: #FFF4E6; border: 1px solid #FFB84D; border-radius: 4px;">
|
||||
<div class="test-env-notice__content" style="display: flex; align-items: flex-start; gap: 8px;">
|
||||
<svg class="test-env-notice__icon" style="flex-shrink: 0; margin-top: 2px; width: 20px; height: 20px;" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M10 0C4.48 0 0 4.48 0 10C0 15.52 4.48 20 10 20C15.52 20 20 15.52 20 10C20 4.48 15.52 0 10 0ZM11 15H9V13H11V15ZM11 11H9V5H11V11Z" fill="#FF9800"/>
|
||||
</svg>
|
||||
<div class="test-env-notice__text-container" style="flex: 1;">
|
||||
<p style="margin: 0; font-size: 14px; color: #E65100; font-weight: 500;">테스트 환경 안내</p>
|
||||
<p style="margin: 4px 0 0 0; font-size: 13px; color: #5D4037; line-height: 1.5;">
|
||||
현재 테스트 환경입니다. 실제로 이메일이 발송되지 않으며, 인증번호는
|
||||
<strong style="color: #E65100; font-family: monospace; font-size: 14px; font-weight: bold;">${authNumber}</strong>
|
||||
입니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 기존 안내 메시지 제거
|
||||
$('.test-env-notice').remove();
|
||||
|
||||
// authCodeContainer 바로 앞에 안내 메시지 삽입
|
||||
const authCodeContainer = document.getElementById('authCodeContainer');
|
||||
if (authCodeContainer) {
|
||||
const noticeDiv = document.createElement('div');
|
||||
noticeDiv.className = 'form-row';
|
||||
noticeDiv.innerHTML = `<div class="form-label-wrapper label-offset"></div><div class="form-field-wrapper">${noticeHtml}</div>`;
|
||||
authCodeContainer.parentNode.insertBefore(noticeDiv, authCodeContainer);
|
||||
}
|
||||
}
|
||||
|
||||
// 인증코드 받기 버튼 클릭
|
||||
$('.btn_send_code').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const $button = $(this);
|
||||
$button.prop('disabled', true).text('발송 중...');
|
||||
|
||||
$.ajax({
|
||||
url: /*[[@{/signup/send-verification-code}]]*/ '/signup/send-verification-code',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: '{}',
|
||||
headers: {
|
||||
[csrfHeaderName]: csrfToken
|
||||
},
|
||||
success: function(response) {
|
||||
if (!response.valid) {
|
||||
customPopups.showAlert(response.message || '인증코드 발송에 실패했습니다.');
|
||||
$button.prop('disabled', false).text('인증코드 받기');
|
||||
return;
|
||||
}
|
||||
|
||||
// 테스트 환경 인증번호 표시
|
||||
if (response.authNumber) {
|
||||
displayTestAuthNotice(response.authNumber);
|
||||
}
|
||||
|
||||
customPopups.showAlert('인증코드가 이메일로 발송되었습니다.');
|
||||
$('#authCodeContainer').slideDown(300);
|
||||
$('#authCode').prop('readonly', false);
|
||||
$('.btn_verify_code').prop('disabled', false);
|
||||
startTimer(300); // 5분 타이머
|
||||
startResendTimer(); // 재발송 타이머 시작
|
||||
},
|
||||
error: function() {
|
||||
customPopups.showAlert('인증코드 발송에 실패했습니다.');
|
||||
$button.prop('disabled', false).text('인증코드 받기');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 인증코드 확인 버튼 클릭
|
||||
$('.btn_verify_code').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const authCode = $('#authCode').val().trim();
|
||||
|
||||
if (!authCode) {
|
||||
customPopups.showAlert('인증코드를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (authCode.length !== 6) {
|
||||
customPopups.showAlert('6자리 인증코드를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: /*[[@{/signup/verify-email-code}]]*/ '/signup/verify-email-code',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ code: authCode }),
|
||||
headers: {
|
||||
[csrfHeaderName]: csrfToken
|
||||
},
|
||||
success: function(response) {
|
||||
if (!response.valid) {
|
||||
customPopups.showAlert(response.message || '인증코드가 일치하지 않습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(timerInterval);
|
||||
clearInterval(resendTimerInterval);
|
||||
isVerified = true;
|
||||
|
||||
// 인증 완료 UI 업데이트
|
||||
$('#authCode').prop('disabled', true);
|
||||
$('.btn_verify_code').prop('disabled', true);
|
||||
$('.btn_send_code').prop('disabled', true);
|
||||
$('#certify_time').text('인증 완료').css('color', '#6BCF7F');
|
||||
|
||||
customPopups.showAlert('이메일 인증이 완료되었습니다! 로그인 후 이용해 주세요.');
|
||||
|
||||
// 알림 확인 후 로그인 페이지로 이동
|
||||
setTimeout(function() {
|
||||
location.href = /*[[@{/login}]]*/ '/login';
|
||||
}, 2000);
|
||||
},
|
||||
error: function() {
|
||||
customPopups.showAlert('인증코드 확인에 실패했습니다.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 인증코드 입력 필드 숫자만 허용
|
||||
$('#authCode').on('input', function() {
|
||||
$(this).val($(this).val().replace(/[^0-9]/g, ''));
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
||||
@@ -71,6 +71,28 @@
|
||||
<div class="editor-content" th:utext="${privacyCollect?.contents ?: '개인정보수집동의서 내용을 불러올 수 없습니다.'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notification Consent (모델에 notificationConsent가 있을 때만 노출) -->
|
||||
<th:block th:if="${notificationConsent != null}">
|
||||
<div class="agreement-item-row">
|
||||
<label class="agreement-checkbox-label">
|
||||
<input type="checkbox" name="notificationConsent" id="notificationConsent" class="agreement-checkbox-input" required>
|
||||
<span class="agreement-checkbox-custom"></span>
|
||||
<span class="agreement-checkbox-text">
|
||||
<span class="agreement-required">[필수]</span>
|
||||
<span class="agreement-title">알림 수신 동의</span>
|
||||
</span>
|
||||
</label>
|
||||
<button type="button" class="agreement-toggle-icon" data-target="notificationConsentContent">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="notificationConsentContent" class="agreement-content" style="display:none;">
|
||||
<div class="agreement-scroll">
|
||||
<div class="editor-content" th:utext="${notificationConsent?.contents ?: '알림 수신 동의서 내용을 불러올 수 없습니다.'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
</div>
|
||||
</form>
|
||||
</th:block>
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
const userNameElement = $('#userName');
|
||||
const termsOfUseElement = $('#termsOfUse');
|
||||
const privacyCollectElement = $('#privacyCollect');
|
||||
const notificationConsentElement = $('#notificationConsent');
|
||||
const mobileNumberElement = $('#mobileNumber');
|
||||
const isPasswordValid = $('#isPasswordValid').val() === 'true';
|
||||
const isPasswordMatch = $('#isPasswordMatch').val() === 'true';
|
||||
@@ -115,6 +116,7 @@
|
||||
userNameElement.val().trim() !== '' &&
|
||||
termsOfUseElement.prop('checked') &&
|
||||
privacyCollectElement.prop('checked') &&
|
||||
(notificationConsentElement.length === 0 || notificationConsentElement.prop('checked')) &&
|
||||
mobileNumberElement.val().trim() !== '' &&
|
||||
isAuthVerified;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user