- 회원가입 이메일 인증 페이지 추가 및 이메일 인증 흐름 구현
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled

- 휴대폰 번호 중복 검증 로직 추가 및 프로퍼티 기반 설정 활성화
- 알림 수신 동의 항목 및 뷰 로직 추가
This commit is contained in:
Rinjae
2026-06-22 19:20:03 +09:00
parent 21c500610c
commit 198f6fc82a
11 changed files with 455 additions and 11 deletions
@@ -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">&nbsp;</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;
}