법인 회원 초대 대응

This commit is contained in:
Rinjae
2025-11-24 22:23:04 +09:00
parent a7d94c4d92
commit d7a32e1dcf
16 changed files with 2293 additions and 20 deletions
@@ -70,6 +70,13 @@
<img th:src="@{/img/icon/icon_login_join.png}" alt="">
<a th:href="@{/signup}">회원가입</a>
</li>
<li>
<span>|</span>
</li>
<li class="icon_join">
<img th:src="@{/img/icon/icon_login_join.png}" alt="">
<a th:href="@{/signup/invitation-code}">초대를 받으셨나요?</a>
</li>
</ul>
</div>
<div class="login_list m-only">
@@ -89,6 +96,12 @@
<li class="icon_join">
<a th:href="@{/signup}">회원가입</a>
</li>
<li>
<span>|</span>
</li>
<li class="icon_join">
<a th:href="@{/signup/invitation-code}">초대를 받으셨나요?</a>
</li>
</ul>
</div>
<div th:if="${param.logout}" class="alert alert-info mt-3">
@@ -9,6 +9,21 @@
<h2 class="title">내 정보 관리</h2>
</div>
<!-- 초대 알림 배너 -->
<div th:if="${hasPendingInvitation}" class="invitation_alert_banner" style="background-color: #f0f8ff; border: 1px solid #2196F3; border-radius: 4px; padding: 15px 20px; margin: 20px 0;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<strong style="color: #1976D2;">법인 회원 초대가 대기 중입니다</strong>
<p style="margin: 5px 0 0 0; color: #666;">기관에서 귀하를 법인 회원으로 초대했습니다. 초대를 수락하면 해당 기관의 법인 회원으로 전환됩니다.</p>
</div>
<div>
<a th:href="@{/signup/decision(invitation=${invitationToken})}" class="common_btn_type_1 blue" style="white-space: nowrap;">
<span>초대 확인</span>
</a>
</div>
</div>
</div>
<div class="inner i_cs h_inner8">
<form id="updateForm" method="post" th:action="@{/mypage/update}" th:object="${user}">
<div class="terms_box user_top">
@@ -0,0 +1,131 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/kbank_base_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<div class="sub_title2">
<h2 class="title add2">초대 코드 입력</h2>
</div>
<div class="inner i_cs9 h_inner9">
<div class="form_type">
<div class="con_title">
<p>법인 기관으로부터 받으신 8자리 초대 코드를 입력해 주세요.</p>
</div>
<form id="invitationCodeForm" method="post" th:action="@{/signup/invitation-code}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="form_type_box">
<div class="info1 form_top">
<p class="title">
<span>초대 코드</span>
</p>
<div class="info_line info_add">
<div class="info_box1 add add2">
<!-- <input type="text"-->
<!-- id="invitationCode"-->
<!-- name="invitationCode"-->
<!-- class="common_input_type_1 h_inp"-->
<!-- placeholder="8자리 초대 코드를 입력해 주세요"-->
<!-- maxlength="8"-->
<!-- pattern="[A-Z0-9]{8}"-->
<!-- title="8자리 영문 대문자와 숫자로 입력해주세요"-->
<!-- required-->
<!-- style="text-transform: uppercase;">-->
<input type="text"
id="invitationCode"
name="invitationCode"
class="common_input_type_1 h_inp"
placeholder="8자리 초대 코드를 입력해 주세요"
maxlength="8"
required
style="text-transform: uppercase;">
</div>
</div>
</div>
<div class="info_txt" style="margin-top: 25px;">
<p>※ 초대 코드는 이메일로 전달받으실 수 있습니다.</p>
<p>※ 초대 코드는 대소문자를 구분하지 않으며, 8자리 영문자와 숫자로 구성되어 있습니다.</p>
</div>
</div>
<!-- 에러 메시지 표시 영역 -->
<div th:if="${error}" class="error_message" style="color: #d32f2f; margin: 25px 0 15px 0; padding: 10px; background-color: #ffebee; border-radius: 4px;">
<p th:text="${error}"></p>
</div>
<!-- 브루트포스 방지: 시도 횟수 표시 -->
<div th:if="${attemptsRemaining != null and attemptsRemaining <= 3}"
class="warning_message"
style="color: #f57c00; margin: 25px 0 15px 0; padding: 10px; background-color: #fff3e0; border-radius: 4px;">
<p>남은 시도 횟수: <strong th:text="${attemptsRemaining}"></strong></p>
<p>5회 실패 시 15분간 입력이 제한됩니다.</p>
</div>
<!-- 브루트포스 방지: 계정 잠금 메시지 -->
<div th:if="${lockoutTime != null}"
class="error_message"
style="color: #d32f2f; margin: 25px 0 15px 0; padding: 10px; background-color: #ffebee; border-radius: 4px;">
<p>잘못된 초대 코드를 5회 입력하셨습니다.</p>
<p th:text="${lockoutTime} + ' 후에 다시 시도해 주세요.'"></p>
</div>
<div class="btn_login">
<button type="submit" class="common_btn_type_1 h_btn" th:disabled="${lockoutTime != null}" style="width: 100%; border: none; cursor: pointer;">
<span>확인</span>
</button>
</div>
</form>
<div class="login_list pc-only" style="margin-top: 20px;">
<ul>
<li>
<a th:href="@{/login}">로그인 페이지로 돌아가기</a>
</li>
<li>
<span>|</span>
</li>
<li>
<a th:href="@{/signup}">회원가입</a>
</li>
</ul>
</div>
<div class="login_list m-only" style="margin-top: 20px;">
<ul>
<li>
<a th:href="@{/login}">로그인 페이지로 돌아가기</a>
</li>
<li>
<span>|</span>
</li>
<li>
<a th:href="@{/signup}">회원가입</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<th:block layout:fragment="contentScript">
<script>
$(document).ready(function() {
// 초대 코드 입력 필드를 대문자로 자동 변환
$('#invitationCode').on('input', function() {
this.value = this.value.toUpperCase();
});
// 성공 메시지가 있으면 표시
var successMsg = [[${success}]];
if (successMsg) {
customPopups.showAlert(successMsg);
}
});
</script>
</th:block>
</body>
</html>
@@ -18,7 +18,7 @@
<p class="tit_txt">
<span th:text="${userName}"></span>님 안녕하세요.<br><br>
<span th:text="${orgName}"></span>에서 귀하를 Kbank API Portal 법인회원으로 초대하였습니다.<br>
<br> Kbank와 함께 금융서비스를 손쉽게 개발해 보세요.<br><br>
<br> 광주은행과 함께 금융서비스를 손쉽게 개발해 보세요.<br><br>
로그인 하여 법인회원 전환을 진행하세요.<br>
</p>
</div>
@@ -67,9 +67,10 @@
<td th:text="${user.maskedEmailAddr}">이메일 아이디</td>
<td>
</td>
<td></td>
<td>초대 대기 중</td>
<td>
<div class="btn_wrap btn_set">
<a href="#none" class="common_btn_type_4 resend_invitation"><span>재발송</span></a>
<a href="#none" class="common_btn_type_4 cancel_invitation"><span>초대취소</span></a>
</div>
</td>
@@ -103,8 +104,10 @@
<div class="contents">
<p class="con_txt2">추가할 이용자 e-mail 주소를 입력해 주세요.</p>
<div class="inp_box">
<input type="text" class="common_input_type_1 w_input" id="new_user_email"
placeholder="이용자 e-mail 주소">
<input type="email" class="common_input_type_1 w_input" id="new_user_email"
placeholder="이용자 e-mail 주소"
pattern="[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}"
title="올바른 이메일 형식을 입력해주세요 (예: user@example.com)">
</div>
</div>
</div>
@@ -208,6 +211,13 @@
return;
}
// 이메일 형식 검증
const emailPattern = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;
if (!emailPattern.test(email)) {
customPopups.showAlert('올바른 이메일 형식을 입력해주세요. (예: user@example.com)');
return;
}
$.ajax({
url: '/users/invite',
type: 'POST',
@@ -292,6 +302,7 @@
const activateButtons = document.querySelectorAll('.activate_user');
const inactivateButtons = document.querySelectorAll('.inactivate_user');
const cancelInvitationButtons = document.querySelectorAll('.cancel_invitation');
const resendInvitationButtons = document.querySelectorAll('.resend_invitation');
activateButtons.forEach(button => {
button.addEventListener('click', handleUserStatusChange);
@@ -305,6 +316,10 @@
button.addEventListener('click', handleCancelInvitation);
});
resendInvitationButtons.forEach(button => {
button.addEventListener('click', handleResendInvitation);
});
function handleUserStatusChange(event) {
const row = event.target.closest('tr');
const userId = row.querySelector('a').getAttribute('href').split('=')[1];
@@ -361,6 +376,38 @@
});
}
function handleResendInvitation(event) {
const row = event.target.closest('tr');
const userId = row.querySelector('td[data-id]').getAttribute('data-id');
const userName = row.querySelector('td:nth-child(3)').textContent;
customPopups.showConfirm(`${userName}에게 초대를 재발송하시겠습니까?`, function (selection) {
if (selection) {
$.ajax({
url: '/users/resend-invitation',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({id: userId})
}).done(function (response) {
customPopups.showAlert(response.msg, function () {
location.reload(); // Reload the page to reflect changes
});
}).fail(function (jqXHR, textStatus, errorThrown) {
let errorMessage;
try {
const errorResponse = JSON.parse(jqXHR.responseText);
errorMessage = errorResponse.msg || '초대 재발송 중 오류가 발생했습니다';
} catch (e) {
errorMessage = '초대 재발송 중 오류가 발생했습니다: ' + errorThrown;
}
customPopups.showAlert(errorMessage);
}).always(function () {
$('#customConfirm').hide();
});
}
});
}
});
</script>
</th:block>