- 인증번호 재발송 제한 메시지 개선 - 남은 초 표시 추가
- 비밀번호 확인 필드명 일괄 변경(password2 → confirmPassword) - 법인 관리자 탈퇴 제한 경고 메시지 추가 및 관련 로직 수정 - 임시 비밀번호 발급 시 기존 비밀번호 이력 저장 로직 추가
This commit is contained in:
@@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -104,9 +105,15 @@ public class AuthNumberServiceImpl implements AuthNumberService {
|
|||||||
private void validateResendTime(String recipientKey) {
|
private void validateResendTime(String recipientKey) {
|
||||||
storage.getAuthNumber(recipientKey).ifPresent(existingAuth -> {
|
storage.getAuthNumber(recipientKey).ifPresent(existingAuth -> {
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
if (existingAuth.getExpiresAt().minusSeconds(authNumberExpirationTime)
|
LocalDateTime resendAvailableAt = existingAuth.getExpiresAt()
|
||||||
.plusSeconds(resendLimitSeconds).isAfter(now)) {
|
.minusSeconds(authNumberExpirationTime)
|
||||||
throw new AuthNumberException("잠시 후에 다시 시도해 주세요.");
|
.plusSeconds(resendLimitSeconds);
|
||||||
|
if (resendAvailableAt.isAfter(now)) {
|
||||||
|
long remainingMillis = Duration.between(now, resendAvailableAt).toMillis();
|
||||||
|
long remainingSeconds = Math.max(1L, (remainingMillis + 999L) / 1000L);
|
||||||
|
throw new AuthNumberException(
|
||||||
|
String.format("인증번호 재발송 제한이 적용 중입니다. %d초 후 다시 시도해 주세요.",
|
||||||
|
remainingSeconds));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -35,8 +35,8 @@ public class UserRegisterRestController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/check_password_match")
|
@PostMapping("/check_password_match")
|
||||||
public ResponseEntity<ValidationResponse> checkPasswordMatch(@RequestParam String password, @RequestParam String password2) {
|
public ResponseEntity<ValidationResponse> checkPasswordMatch(@RequestParam String password, @RequestParam String confirmPassword) {
|
||||||
return ResponseEntity.ok(userRegisterFacade.checkPasswordMatch(password, password2));
|
return ResponseEntity.ok(userRegisterFacade.checkPasswordMatch(password, confirmPassword));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/register/confirm_password")
|
@PostMapping("/register/confirm_password")
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import org.hibernate.validator.constraints.NotEmpty;
|
|||||||
|
|
||||||
|
|
||||||
@AuthNumberMatch(recipient = "loginId", authField = "authNumber")
|
@AuthNumberMatch(recipient = "loginId", authField = "authNumber")
|
||||||
@PasswordMatch(input = "password", confirm = "password2")
|
@PasswordMatch(input = "password", confirm = "confirmPassword")
|
||||||
@Data
|
@Data
|
||||||
@PasswordRule(password = "password", loginId = "loginId", mobile = "mobileNumber")
|
@PasswordRule(password = "password", loginId = "loginId", mobile = "mobileNumber")
|
||||||
public class PortalUserRegistrationDTO {
|
public class PortalUserRegistrationDTO {
|
||||||
@@ -31,8 +31,6 @@ public class PortalUserRegistrationDTO {
|
|||||||
*/
|
*/
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
private String password2;
|
|
||||||
|
|
||||||
@CellPhone
|
@CellPhone
|
||||||
private String mobileNumber;
|
private String mobileNumber;
|
||||||
|
|
||||||
|
|||||||
@@ -140,11 +140,11 @@ public class UserFacadeImpl implements UserFacade {
|
|||||||
public void withdrawUser(String userId, String withdrawalReason) {
|
public void withdrawUser(String userId, String withdrawalReason) {
|
||||||
PortalUser user = portalUserService.findById(userId);
|
PortalUser user = portalUserService.findById(userId);
|
||||||
|
|
||||||
// 법인 관리자 탈퇴 제한
|
// 법인 관리자는 권한 이관 전 탈퇴할 수 없다.
|
||||||
if (user.getRoleCode() == PortalUserEnums.RoleCode.ROLE_CORP_MANAGER) {
|
if (user.getRoleCode() == PortalUserEnums.RoleCode.ROLE_CORP_MANAGER) {
|
||||||
if(portalUserService.checkOrgHasOtherUsers(user.getPortalOrg())){
|
throw new IllegalArgumentException(
|
||||||
throw new IllegalArgumentException("법인 관리자권한을 다른 개발자에게 위임하신 후 탈퇴가 가능합니다.");
|
"법인 관리자는 회원 탈퇴를 할 수 없습니다. "
|
||||||
}
|
+ "관리자 권한을 다른 사용자에게 이관하거나 담당자에게 연락해 주세요.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 약관 동의 정보 삭제
|
// 약관 동의 정보 삭제
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public interface UserRegisterFacade {
|
|||||||
|
|
||||||
ValidationResponse checkPassword(String password, String loginId, String mobileNumber);
|
ValidationResponse checkPassword(String password, String loginId, String mobileNumber);
|
||||||
|
|
||||||
ValidationResponse checkPasswordMatch(String password, String password2);
|
ValidationResponse checkPasswordMatch(String password, String confirmPassword);
|
||||||
|
|
||||||
ValidationResponse verifyPassword(String loginId, String confirmPassword);
|
ValidationResponse verifyPassword(String loginId, String confirmPassword);
|
||||||
|
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ValidationResponse checkPasswordMatch(String password, String password2) {
|
public ValidationResponse checkPasswordMatch(String password, String confirmPassword) {
|
||||||
boolean isMatch = password.equals(password2);
|
boolean isMatch = password.equals(confirmPassword);
|
||||||
String message = isMatch ? "비밀번호가 일치합니다." : "비밀번호가 일치하지 않습니다.";
|
String message = isMatch ? "비밀번호가 일치합니다." : "비밀번호가 일치하지 않습니다.";
|
||||||
return new ValidationResponse(isMatch, message);
|
return new ValidationResponse(isMatch, message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,17 @@ public class PasswordService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link #updatePassword} 를 거치지 않고 비밀번호 해시를 직접 바꾸는 지점(예: 비밀번호 초기화로
|
||||||
|
* 임시 비밀번호 발급 — PortalUserAuthService.resetPassword)이 <b>덮어쓰기 직전</b>에 호출해,
|
||||||
|
* 지금 버려지는 비밀번호를 이력에 남긴다. 이걸 빼먹으면 재사용 금지(최근 5회) 검증이 그 비밀번호를
|
||||||
|
* 전혀 모른 채로 남아 있어, 초기화 이후 바로 예전 비밀번호로 되돌리는 게 허용되는 보안 허점이 된다.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void recordExternalPasswordChange(String userId, String previousPasswordHash) {
|
||||||
|
savePasswordHistory(userId, previousPasswordHash);
|
||||||
|
}
|
||||||
|
|
||||||
private void checkPasswordHistory(String userId, String newPassword) {
|
private void checkPasswordHistory(String userId, String newPassword) {
|
||||||
List<UserPasswordHistory> passwordHistories = passwordHistoryRepository.findRecentPasswordsByUserId(userId);
|
List<UserPasswordHistory> passwordHistories = passwordHistoryRepository.findRecentPasswordsByUserId(userId);
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ public class PortalUserAuthService implements UserDetailsService {
|
|||||||
private final MessageRequestRepository messageRequestRepository;
|
private final MessageRequestRepository messageRequestRepository;
|
||||||
private final EncryptionUtil encryptionUtil;
|
private final EncryptionUtil encryptionUtil;
|
||||||
private final LoginFinalizer loginFinalizer;
|
private final LoginFinalizer loginFinalizer;
|
||||||
|
private final PasswordService passwordService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(noRollbackFor = UsernameNotFoundException.class)
|
@Transactional(noRollbackFor = UsernameNotFoundException.class)
|
||||||
@@ -164,6 +165,10 @@ public class PortalUserAuthService implements UserDetailsService {
|
|||||||
.orElseThrow(() -> new UserNotFoundException("일치하는 사용자 정보를 찾을 수 없습니다."));
|
.orElseThrow(() -> new UserNotFoundException("일치하는 사용자 정보를 찾을 수 없습니다."));
|
||||||
|
|
||||||
String tempPassword = EncryptionUtil.generateNewPassword();
|
String tempPassword = EncryptionUtil.generateNewPassword();
|
||||||
|
// 지금 버려지는(임시 비밀번호로 교체되는) 비밀번호를 이력에 남긴다 — 안 남기면 재사용 금지
|
||||||
|
// (최근 5회) 검증이 이 비밀번호를 모른 채로 남아, 초기화 직후 바로 예전 비밀번호로 되돌리는
|
||||||
|
// 것이 허용되는 보안 허점이 생긴다.
|
||||||
|
passwordService.recordExternalPasswordChange(portalUser.getId(), portalUser.getPasswordHash());
|
||||||
portalUser.setPasswordHash(passwordEncoder.encode(tempPassword));
|
portalUser.setPasswordHash(passwordEncoder.encode(tempPassword));
|
||||||
// 임시 비밀번호 발급 → 변경일을 null 로 초기화해 로그인 시 강제 비밀번호 변경을 유도한다
|
// 임시 비밀번호 발급 → 변경일을 null 로 초기화해 로그인 시 강제 비밀번호 변경을 유도한다
|
||||||
// (LoginFinalizer.applyPostLoginState 의 passwordChangeDate == null 분기)
|
// (LoginFinalizer.applyPostLoginState 의 passwordChangeDate == null 분기)
|
||||||
|
|||||||
@@ -754,15 +754,15 @@
|
|||||||
if (withdrawalBtn) {
|
if (withdrawalBtn) {
|
||||||
withdrawalBtn.addEventListener('click', (e) => {
|
withdrawalBtn.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
customPopups.showWithdrawal();
|
customPopups.showAlert(
|
||||||
|
'법인 관리자는 회원 탈퇴를 할 수 없습니다.<br>' +
|
||||||
|
'관리자 권한을 다른 사용자에게 이관하거나 담당자에게 연락해 주세요.'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</th:block>
|
</th:block>
|
||||||
<section layout:fragment="pagePopups">
|
|
||||||
<th:block th:replace="~{fragment/popup/withdrawalPopup :: withdrawalPopup}"></th:block>
|
|
||||||
</section>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -110,7 +110,7 @@
|
|||||||
비밀번호 확인 <span class="required-badge">필수</span>
|
비밀번호 확인 <span class="required-badge">필수</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="org-form-input-wrapper">
|
<div class="org-form-input-wrapper">
|
||||||
<input type="password" name="password2" id="password2" class="org-form-input"
|
<input type="password" name="confirmPassword" id="confirmPassword" class="org-form-input"
|
||||||
th:placeholder="#{portalUser.Register.passConfirm}">
|
th:placeholder="#{portalUser.Register.passConfirm}">
|
||||||
<input type="hidden" name="isPasswordMatch" id="isPasswordMatch" />
|
<input type="hidden" name="isPasswordMatch" id="isPasswordMatch" />
|
||||||
<div id="password-match-validation" class="org-validation-message"></div>
|
<div id="password-match-validation" class="org-validation-message"></div>
|
||||||
@@ -411,9 +411,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 비밀번호 확인 검증
|
// 비밀번호 확인 검증
|
||||||
$('#password2').on('blur', function () {
|
$('#confirmPassword').on('blur', function () {
|
||||||
let password2 = $(this).val();
|
let confirmPassword = $(this).val();
|
||||||
if (!password2) {
|
if (!confirmPassword) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,7 +422,7 @@
|
|||||||
type: 'POST',
|
type: 'POST',
|
||||||
data: {
|
data: {
|
||||||
password: $('#password').val(),
|
password: $('#password').val(),
|
||||||
password2: password2,
|
confirmPassword: confirmPassword,
|
||||||
_csrf: $('input[name="_csrf"]').val()
|
_csrf: $('input[name="_csrf"]').val()
|
||||||
},
|
},
|
||||||
success: function (response) {
|
success: function (response) {
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
// 시나리오별 필수 필드 정의
|
// 시나리오별 필수 필드 정의
|
||||||
const requiredFieldsByScenario = {
|
const requiredFieldsByScenario = {
|
||||||
new: {
|
new: {
|
||||||
user: ['loginId', 'userName', 'password', 'password2', 'mobileNumber', 'authNumber'],
|
user: ['loginId', 'userName', 'password', 'confirmPassword', 'mobileNumber', 'authNumber'],
|
||||||
org: ['compRegNo', 'corpRegNo', 'orgName', 'compRegFile', 'files']
|
org: ['compRegNo', 'corpRegNo', 'orgName', 'compRegFile', 'files']
|
||||||
},
|
},
|
||||||
retain: {
|
retain: {
|
||||||
@@ -228,6 +228,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add confirmPassword manually based on the scenario
|
// Add confirmPassword manually based on the scenario
|
||||||
|
// (new 시나리오는 #confirmPassword 필드 자체가 있어 위 공통 user 필드 루프가 그대로 처리한다.
|
||||||
|
// 서버는 시나리오 무관하게 confirmPassword 단일 필드로 비밀번호 확인을 검증한다 —
|
||||||
|
// OrgRegisterFacadeImpl.registerNewOrgUser 참고.)
|
||||||
if (registrationScenario === 'retain') {
|
if (registrationScenario === 'retain') {
|
||||||
const passwordConfirmIndividual = document.getElementById('passwordConfirmIndividual');
|
const passwordConfirmIndividual = document.getElementById('passwordConfirmIndividual');
|
||||||
if (passwordConfirmIndividual) {
|
if (passwordConfirmIndividual) {
|
||||||
|
|||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
package com.eactive.apim.portal.apps.auth.service;
|
||||||
|
|
||||||
|
import com.eactive.apim.portal.portaluser.entity.TwoFactorAuth;
|
||||||
|
import com.eactive.apim.portal.portaluser.service.AuthNumberException;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class AuthNumberServiceImplTest {
|
||||||
|
|
||||||
|
private static final Pattern RETRY_SECONDS = Pattern.compile(
|
||||||
|
"인증번호 재발송 제한이 적용 중입니다\\. (\\d+)초 후 다시 시도해 주세요\\.");
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private AuthNumberStorage storage;
|
||||||
|
@Mock
|
||||||
|
private AuthNumberGenerator generator;
|
||||||
|
@Mock
|
||||||
|
private MessageSender messageSender;
|
||||||
|
|
||||||
|
private AuthNumberServiceImpl service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
service = new AuthNumberServiceImpl(storage, generator, messageSender);
|
||||||
|
ReflectionTestUtils.setField(service, "authNumberExpirationTime", 300);
|
||||||
|
ReflectionTestUtils.setField(service, "resendLimitSeconds", 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resendLimitMessageIncludesRemainingSeconds() {
|
||||||
|
String recipient = "01099121100";
|
||||||
|
TwoFactorAuth existing = new TwoFactorAuth(
|
||||||
|
recipient, "123456", LocalDateTime.now().plusSeconds(300));
|
||||||
|
when(storage.getAuthNumber(recipient)).thenReturn(Optional.of(existing));
|
||||||
|
|
||||||
|
AuthNumberException exception = assertThrows(
|
||||||
|
AuthNumberException.class,
|
||||||
|
() -> service.sendRequestAuthNumber(recipient, "SMS")
|
||||||
|
);
|
||||||
|
|
||||||
|
Matcher matcher = RETRY_SECONDS.matcher(exception.getMessage());
|
||||||
|
assertTrue(matcher.matches(), "남은 재시도 초가 안내 메시지에 포함되어야 함");
|
||||||
|
long remainingSeconds = Long.parseLong(matcher.group(1));
|
||||||
|
assertTrue(remainingSeconds >= 1 && remainingSeconds <= 30,
|
||||||
|
"남은 초는 1~30 범위여야 함: " + remainingSeconds);
|
||||||
|
assertNull(exception.getReason());
|
||||||
|
verifyNoInteractions(generator, messageSender);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package com.eactive.apim.portal.apps.user;
|
||||||
|
|
||||||
|
import com.eactive.apim.portal.apps.agreements.service.AgreementsFacade;
|
||||||
|
import com.eactive.apim.portal.apps.user.facade.MessageRequestFacade;
|
||||||
|
import com.eactive.apim.portal.apps.user.facade.UserFacadeImpl;
|
||||||
|
import com.eactive.apim.portal.apps.user.mapper.PortalUserMapper;
|
||||||
|
import com.eactive.apim.portal.apps.user.service.PasswordService;
|
||||||
|
import com.eactive.apim.portal.apps.user.service.PortalOrgService;
|
||||||
|
import com.eactive.apim.portal.apps.user.service.PortalUserService;
|
||||||
|
import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
||||||
|
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode;
|
||||||
|
import com.eactive.apim.portal.template.service.MessageHandlerService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class UserFacadeImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PortalUserService portalUserService;
|
||||||
|
@Mock
|
||||||
|
private PortalOrgService portalOrgService;
|
||||||
|
@Mock
|
||||||
|
private PasswordService passwordService;
|
||||||
|
@Mock
|
||||||
|
private PortalUserMapper portalUserMapper;
|
||||||
|
@Mock
|
||||||
|
private MessageHandlerService messageHandlerService;
|
||||||
|
@Mock
|
||||||
|
private AgreementsFacade agreementsFacade;
|
||||||
|
@Mock
|
||||||
|
private MessageRequestFacade messageRequestFacade;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private UserFacadeImpl userFacade;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void corporateManagerCannotWithdraw() {
|
||||||
|
PortalUser manager = new PortalUser();
|
||||||
|
manager.setId("manager-1");
|
||||||
|
manager.setRoleCode(RoleCode.ROLE_CORP_MANAGER);
|
||||||
|
when(portalUserService.findById("manager-1")).thenReturn(manager);
|
||||||
|
|
||||||
|
IllegalArgumentException exception = assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> userFacade.withdrawUser("manager-1", "withdrawal reason")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"법인 관리자는 회원 탈퇴를 할 수 없습니다. "
|
||||||
|
+ "관리자 권한을 다른 사용자에게 이관하거나 담당자에게 연락해 주세요.",
|
||||||
|
exception.getMessage()
|
||||||
|
);
|
||||||
|
verifyNoInteractions(agreementsFacade, messageRequestFacade);
|
||||||
|
verify(portalUserService, never()).deleteUser(manager, "withdrawal reason");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void corporateUserCanWithdraw() {
|
||||||
|
PortalUser user = new PortalUser();
|
||||||
|
user.setId("user-1");
|
||||||
|
user.setLoginId("corp-user@example.com");
|
||||||
|
user.setUserName("법인 사용자");
|
||||||
|
user.setRoleCode(RoleCode.ROLE_CORP_USER);
|
||||||
|
when(portalUserService.findById("user-1")).thenReturn(user);
|
||||||
|
|
||||||
|
userFacade.withdrawUser("user-1", "withdrawal reason");
|
||||||
|
|
||||||
|
verify(agreementsFacade).deleteUserAgreements("user-1");
|
||||||
|
verify(messageRequestFacade).deleteUserMessage("법인 사용자", "corp-user@example.com");
|
||||||
|
verify(portalUserService).deleteUser(user, "withdrawal reason");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user