회원가입 디자인 적용

This commit is contained in:
현성필
2025-11-15 18:38:03 +09:00
parent cadac012e6
commit 42100e8a1a
18 changed files with 3088 additions and 1398 deletions
@@ -1,65 +1,76 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/kbank_base_layout}">
layout:decorate="~{layout/kjbank_signup_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<script th:src="@{/js/htmx.min.js}"></script>
<div class="content_wrap">
<div class="sub_title2">
<h2 class="title add" th:if="${!isInvited}">개인회원가입</h2>
<h2 class="title add" th:if="${isInvited}">법인회원가입</h2>
</div>
<div class="inner i_cs h_inner2">
<div class="top_info" th:if="${!isInvited}">
<ul>
<li>서비스 또는 API 사용을 원하실 경우 법인회원 승인 후 이용하실 수 있습니다.</li>
</ul>
</div>
<!-- 약관동의 섹션 -->
<input type="hidden" id="csrf" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<th:block th:replace="~{apps/register/userAgreementContent :: agreementContent(${termsOfUse}, ${privacyCollect})}"></th:block>
<!-- 기본 정보 입력 섹션 -->
<form name="portalUser" id="registerForm" role="form" th:action="@{/signup/portalUser}"
th:object="${portalUser}" method="post">
<!-- 법인 정보 섹션, 초대받은 경우에만 -->
<div class="terms_box" th:if="${isInvited}">
<p>법인 정보</p>
</div>
<section layout:fragment="title">
<div class="org-page-title-banner">
<img th:src="@{/img/user_register_title.png}" alt="개인회원가입" class="title-image">
<h1 th:text="${!isInvited ? '개인회원가입' : '법인회원가입'}">개인회원가입</h1>
</div>
</section>
<div class="form_type" th:if="${isInvited}">
<div class="info1">
<p class="title w_tit">
<span>법인명</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="text" name="companyName" class="common_input_type_1"
th:classappend="${isInvited ? 'input-readonly' : ''}" th:value="${orgName}" readonly>
</div>
</div>
</div>
</div>
<!-- end 법인 정보 섹션, 초대받은 경우에만 -->
<div class="terms_box">
<p>기본 정보</p>
<span class="dot">필수 입력란입니다</span>
</div>
<div class="form_type" th:if="${registrationType == isInvited ? 'corporate' :'personal'}">
<th:block th:replace="~{apps/register/components/commonUserInfoForm :: commonUserInfo}"></th:block>
<th:block th:replace="~{apps/register/components/newUserInfoForm :: newUserForm}"></th:block>
</div>
<div class="form_type">
<div class="btn_application">
<a type="button" class="common_btn_type_1 gray" id="cancelButton"><span>취소</span></a>
<div class="btn_gap"></div>
<a type="button" class="common_btn_type_1 btn_register"><span>가입신청</span></a>
</div>
</div>
</form>
<section layout:fragment="contentFragment">
<div class="org-register-page">
<div class="org-register-container">
<!-- Hidden Fields -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" name="registrationType" th:value="${registrationType}"/>
<!-- Alert Container -->
<div id="alertContainer"></div>
<!-- Info Notice -->
<div class="org-info-notice" th:if="${!isInvited}">
<ul>
<li>서비스 또는 API 사용을 원하실 경우 법인회원 승인 후 이용하실 수 있습니다.</li>
</ul>
</div>
<!-- Agreement Section -->
<th:block th:replace="~{apps/register/userAgreementContent :: agreementContent(${termsOfUse}, ${privacyCollect})}"></th:block>
<form name="portalUser" id="registerForm" role="form" th:action="@{/signup/portalUser}"
th:object="${portalUser}" method="post">
<!-- Corporate Information Section (invited users only) -->
<div th:if="${isInvited}">
<div class="org-section-header org-section-header--agreement">
<h3>법인 정보</h3>
</div>
<div class="org-form-group">
<label class="org-form-label">법인명</label>
<div class="org-form-input-wrapper">
<input type="text" name="companyName" class="org-form-input" th:value="${orgName}" readonly>
</div>
</div>
</div>
<!-- Basic Information Section -->
<div class="org-section-header org-section-header--agreement" th:style="${isInvited ? 'margin-top: 48px;' : ''}">
<h3>기본 정보</h3>
<span class="required-badge">필수 입력</span>
</div>
<div th:if="${registrationType == (isInvited ? 'corporate' : 'personal')}">
<th:block th:replace="~{apps/register/components/commonUserInfoForm :: commonUserInfo}"></th:block>
<th:block th:replace="~{apps/register/components/newUserInfoForm :: newUserForm}"></th:block>
</div>
<!-- Action Buttons -->
<div class="org-action-buttons">
<button type="button" class="org-btn org-btn-secondary" id="cancelButton">취소</button>
<button type="button" class="org-btn org-btn-primary btn_register">가입신청</button>
</div>
</form>
<!-- Loading Overlay -->
<div class="org-loading-overlay" id="loadingOverlay">
<div class="spinner"></div>
</div>
</div>
</div>
</section>
<th:block layout:fragment="contentScript">
<script th:if="${error}" th:inline="javascript">
@@ -70,128 +81,119 @@
<script th:replace="~{apps/register/userAgreementContent :: agreementScript}"></script>
<script th:replace="~{apps/register/components/commonUserInfoForm :: commonUserInfoScript}"></script>
<script th:replace="~{apps/register/components/newUserInfoForm :: newUserScript}"></script>
<script>
let form = document.getElementById('registerForm');
let agreementForm = document.getElementById('agreementForm');
let btn_register = document.querySelector('.btn_register');
let isAuthVerified = false;
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function () {
let form = document.getElementById('registerForm');
let agreementForm = document.getElementById('agreementForm');
let btn_register = document.querySelector('.btn_register');
let isAuthVerified = false;
// 인증 완료 이벤트 리스너
document.addEventListener('authVerified', function() {
isAuthVerified = true;
});
// 인증 완료 이벤트 리스너
document.addEventListener('authVerified', function() {
isAuthVerified = true;
});
// 이메일 유효성 검사 함수
function isValidEmail(email) {
const safeEmailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
return safeEmailRegex.test(email);
}
// 폼 유효성 검사 함수
function checkFormValidity() {
return isValidEmail($('#loginId').val()) &&
($('#password').val() === $('#password2').val()) &&
($('#userName').val().trim() !== '') &&
$('#termsOfUse').prop('checked') &&
$('#privacyCollect').prop('checked') &&
(combineMobileNumber('#mobileNumber') !== null) &&
isAuthVerified; // 인증 여부 확인
}
let isSubmitting = false;
// 등록 버튼 클릭 이벤트
btn_register.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation();
// Prevent resubmission
if (isSubmitting) {
console.log('Form is already being submitted...');
return;
// 이메일 유효성 검사 함수
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
isSubmitting = true;
btn_register.disabled = true;
// 폼 유효성 검사 함수
function checkFormValidity() {
const loginIdElement = $('#loginId');
const userNameElement = $('#userName');
const termsOfUseElement = $('#termsOfUse');
const privacyCollectElement = $('#privacyCollect');
const mobileNumberElement = $('#mobileNumber');
const isPasswordValid = $('#isPasswordValid').val() === 'true';
const isPasswordMatch = $('#isPasswordMatch').val() === 'true';
form.classList.add('was-validated');
agreementForm.classList.add('was-validated');
return isValidEmail(loginIdElement.val()) &&
isPasswordValid &&
isPasswordMatch &&
userNameElement.val().trim() !== '' &&
termsOfUseElement.prop('checked') &&
privacyCollectElement.prop('checked') &&
mobileNumberElement.val().trim() !== '' &&
isAuthVerified;
}
if (checkFormValidity() && form.checkValidity() && agreementForm.checkValidity()) {
// 약관 동의 폼의 데이터를 hidden input으로 추가
let agreementFormData = new FormData(agreementForm);
for (let pair of agreementFormData.entries()) {
if (!form.querySelector('input[name="' + pair[0] + '"]')) {
let input = document.createElement('input');
input.type = 'hidden';
input.name = pair[0];
input.value = pair[1];
form.appendChild(input);
}
let isSubmitting = false;
// 등록 버튼 클릭 이벤트
btn_register.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation();
// Prevent resubmission
if (isSubmitting) {
console.log('Form is already being submitted...');
return;
}
// 불필요한 hidden input 제거
const unnecessaryFields = ['isPasswordValid', 'isPasswordMatch'];
unnecessaryFields.forEach(fieldName => {
const field = form.querySelector(`input[name="${fieldName}"]`);
if (field) {
field.remove();
isSubmitting = true;
btn_register.disabled = true;
// Show loading overlay
document.getElementById('loadingOverlay').classList.add('active');
try {
form.classList.add('was-validated');
if (agreementForm) {
agreementForm.classList.add('was-validated');
}
});
// authCompletedYn 값 설정
const authCompletedField = document.createElement('input');
authCompletedField.type = 'hidden';
authCompletedField.name = 'authCompletedYn';
authCompletedField.value = isAuthVerified ? 'Y' : 'N';
form.appendChild(authCompletedField);
if (checkFormValidity() && form.checkValidity() && (agreementForm ? agreementForm.checkValidity() : true)) {
// 약관 동의 폼의 데이터를 hidden input으로 추가
if (agreementForm) {
let agreementFormData = new FormData(agreementForm);
for (let pair of agreementFormData.entries()) {
if (!form.querySelector('input[name="' + pair[0] + '"]')) {
let input = document.createElement('input');
input.type = 'hidden';
input.name = pair[0];
input.value = pair[1];
form.appendChild(input);
}
}
}
form.submit();
} else {
customPopups.showAlert('모든 필수 항목을 입력하고 약관에 동의해주세요.');
isSubmitting = false;
btn_register.disabled = false;
}
});
// 불필요한 hidden input 제거
const unnecessaryFields = ['isPasswordValid', 'isPasswordMatch'];
unnecessaryFields.forEach(fieldName => {
const field = form.querySelector(`input[name="${fieldName}"]`);
if (field) {
field.remove();
}
});
// 취소버튼 클릭시
document.getElementById('cancelButton').addEventListener('click', function() {
location.href = '/login';
});
// authCompletedYn 값 설정
const authCompletedField = document.createElement('input');
authCompletedField.type = 'hidden';
authCompletedField.name = 'authCompletedYn';
authCompletedField.value = isAuthVerified ? 'Y' : 'N';
form.appendChild(authCompletedField);
// HTMX 이벤트 리스너 - 모든 HTMX 응답을 중앙에서 처리
document.body.addEventListener('htmx:afterRequest', function(event) {
const targetId = event.detail.target.id;
let response;
try {
response = JSON.parse(event.detail.xhr.response);
} catch (e) {
console.error('응답 JSON 파싱 실패:', e);
customPopups.showAlert('서버 응답을 처리할 수 없습니다.');
return;
}
// 커스텀 헤더를 체크하여 등록 검증 요청 처리
if (event.detail.xhr.getResponseHeader('X-Custom-Handler') === 'registration-validation') {
if (response && response.message) {
if (response.status === 'SUCCESS') {
customPopups.showSuccess(response.message);
form.submit();
} else {
customPopups.showAlert(response.message);
customPopups.showAlert('모든 필수 항목을 입력하고 약관에 동의해주세요.');
isSubmitting = false;
btn_register.disabled = false;
document.getElementById('loadingOverlay').classList.remove('active');
}
} catch (error) {
console.error('Form submission error:', error);
isSubmitting = false;
btn_register.disabled = false;
document.getElementById('loadingOverlay').classList.remove('active');
}
return;
}
});
// 응답 처리 로직
if (targetId === 'email-validation') {
// commonUserInfoForm의 함수 호출
handleEmailValidationResponse(response);
} else if (targetId === 'password-validation' || targetId === 'password-match-validation') {
// 비밀번호 검증은 컴포넌트에서 처리하도록 함
return;
}
// 취소 버튼 클릭 시
document.getElementById('cancelButton').addEventListener('click', function() {
location.href = '/login';
});
});
</script>
</th:block>