PasswordServiceTest 추가 - 최근 비밀번호 중복 검사 로직 수정
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled

TestCleanupService 확장 - 초대/회원 소속 관리 메서드 구현
API 추가 - 초대 삭제 및 소속 제외, 사용자 존재 여부 확인
This commit is contained in:
Rinjae
2026-08-24 14:13:26 +09:00
parent 78b1edff5a
commit c223fa3124
5 changed files with 289 additions and 2 deletions
@@ -34,7 +34,8 @@ public class PasswordService {
.orElseThrow(() -> new IllegalArgumentException("해당 사용자를 찾을 수 없습니다."));
validatePasswordUpdate(user, newPassword, confirmPassword);
checkPasswordHistory(user.getLoginId(), newPassword);
// PTL_USER_PASSWORD_HISTORY.USER_ID 에는 loginId가 아닌 PortalUser.id가 저장된다.
checkPasswordHistory(user.getId(), newPassword);
List<UserPasswordHistory> histories = passwordHistoryRepository.findRecentPasswordsByUserId(user.getId());
if(histories.isEmpty()) {
@@ -17,6 +17,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -105,6 +106,81 @@ public class TestCleanupInternalController {
return ResponseEntity.ok(body);
}
/**
* Playwright 실행 전 동일 이메일 계정의 존재 여부를 확인한다. 조회 전용이며 삭제하지 않는다.
*/
@GetMapping("/user/exists")
public ResponseEntity<Map<String, Object>> checkUserExists(
@RequestParam String email, HttpServletRequest request) {
ResponseEntity<Map<String, Object>> guardFailure = checkGuards(request);
if (guardFailure != null) {
return guardFailure;
}
if (StringUtils.isBlank(email)) {
return badRequest("email 은 필수입니다.");
}
TestCleanupResult result = testCleanupService.checkUserExists(email);
log.info("테스트 정리(user exists) 조회 - email: {}, found: {}, from: {}",
email, result.isFound(), request.getRemoteAddr());
Map<String, Object> body = baseBody();
body.put("email", email);
body.put("userId", result.getTargetId());
body.put("userFound", result.isFound());
return ResponseEntity.ok(body);
}
/**
* 휴대폰 번호로 남아 있는 초대 레코드를 삭제한다. 1020 재실행 전 초대중 중복을 정리하는 용도다.
*/
@PostMapping("/invitation")
public ResponseEntity<Map<String, Object>> deleteInvitations(
@RequestParam String mobile, HttpServletRequest request) {
ResponseEntity<Map<String, Object>> guardFailure = checkGuards(request);
if (guardFailure != null) {
return guardFailure;
}
if (StringUtils.isBlank(mobile)) {
return badRequest("mobile 은 필수입니다.");
}
TestCleanupResult result = testCleanupService.deleteInvitationsByMobile(mobile);
log.info("테스트 정리(invitation) 실행 - mobile: {}, found: {}, from: {}",
mobile, result.isFound(), request.getRemoteAddr());
Map<String, Object> body = baseBody();
body.put("mobile", mobile);
body.put("invitationFound", result.isFound());
body.put("deletedCounts", result.getDeletedCounts());
return ResponseEntity.ok(body);
}
/**
* 휴대폰 번호로 찾은 계정을 법인 소속에서 제외한다. 계정 자체는 삭제하지 않는다.
*/
@PostMapping("/membership")
public ResponseEntity<Map<String, Object>> detachMembership(
@RequestParam String mobile, HttpServletRequest request) {
ResponseEntity<Map<String, Object>> guardFailure = checkGuards(request);
if (guardFailure != null) {
return guardFailure;
}
if (StringUtils.isBlank(mobile)) {
return badRequest("mobile 은 필수입니다.");
}
TestCleanupResult result = testCleanupService.detachUsersFromOrgByMobile(mobile);
log.info("테스트 정리(membership) 실행 - mobile: {}, found: {}, from: {}",
mobile, result.isFound(), request.getRemoteAddr());
Map<String, Object> body = baseBody();
body.put("mobile", mobile);
body.put("membershipFound", result.isFound());
body.put("deletedCounts", result.getDeletedCounts());
return ResponseEntity.ok(body);
}
@PostMapping("/orphans")
public ResponseEntity<Map<String, Object>> cleanOrphans(HttpServletRequest request) {
ResponseEntity<Map<String, Object>> guardFailure = checkGuards(request);
@@ -10,8 +10,11 @@ import com.eactive.apim.portal.djb.webhook.repository.WebhookRequestEventReposit
import com.eactive.apim.portal.djb.webhook.repository.WebhookRequestRepository;
import com.eactive.apim.portal.djb.webhook.repository.entity.WebhookRequest;
import com.eactive.apim.portal.djb.webhook.service.WebhookService;
import com.eactive.apim.portal.common.util.PhoneNumberUtil;
import com.eactive.apim.portal.invitation.repository.UserInvitationRepository;
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode;
import com.eactive.apim.portal.portaluser.repository.PortalUserPrivacyAgreementRepository;
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
import com.eactive.apim.portal.portaluser.repository.UserPasswordHistoryRepository;
@@ -57,6 +60,7 @@ public class TestCleanupService {
private final WebhookRequestApiRepository webhookRequestApiRepository;
private final WebhookRequestEventRepository webhookRequestEventRepository;
private final WebhookService webhookService;
private final UserInvitationRepository userInvitationRepository;
private final TestCleanupNativeQueries nativeQueries;
/**
@@ -66,6 +70,12 @@ public class TestCleanupService {
assertNonProdProfile();
Optional<PortalOrg> orgOpt = portalOrgRepository.findByCompRegNo(compRegNo);
// 가입 화면은 000-00-00001처럼 입력받지만 DB에는 숫자만 저장되는 환경도 있다.
// cleanup API는 두 형식을 모두 받아 이전 E2E 실행 법인을 빠짐없이 정리해야 한다.
String digitsOnlyCompRegNo = compRegNo.replaceAll("\\D", "");
if (!orgOpt.isPresent() && !digitsOnlyCompRegNo.isEmpty() && !digitsOnlyCompRegNo.equals(compRegNo)) {
orgOpt = portalOrgRepository.findByCompRegNo(digitsOnlyCompRegNo);
}
if (!orgOpt.isPresent()) {
return TestCleanupResult.notFound();
}
@@ -73,6 +83,8 @@ public class TestCleanupService {
String orgId = org.getId();
TestCleanupResult result = TestCleanupResult.found(orgId);
result.put("PTL_USER_INVITATION", userInvitationRepository.deleteByOrgId(orgId));
List<PortalUser> users = portalUserRepository.findAllByPortalOrg_Id(orgId);
for (PortalUser user : users) {
result.merge(deleteUserCascadeInternal(user));
@@ -118,6 +130,60 @@ public class TestCleanupService {
return result;
}
/**
* 이메일 기준으로 테스트 계정 존재 여부만 조회한다. 삭제나 데이터 변경은 수행하지 않는다.
*/
@Transactional(readOnly = true)
public TestCleanupResult checkUserExists(String email) {
assertNonProdProfile();
return portalUserRepository.findPortalUserByEmailAddr(email)
.map(user -> TestCleanupResult.found(user.getId()))
.orElseGet(TestCleanupResult::notFound);
}
/**
* 휴대폰 번호로 남은 초대 레코드를 전부 삭제한다. 1020 재실행 전 PENDING 초대 중복을 방지한다.
*/
public TestCleanupResult deleteInvitationsByMobile(String mobile) {
assertNonProdProfile();
String normalizedMobile = PhoneNumberUtil.normalize(mobile);
if (normalizedMobile == null) {
return TestCleanupResult.notFound();
}
long deleted = userInvitationRepository.deleteByInvitationMobile(normalizedMobile);
TestCleanupResult result = deleted > 0 ? TestCleanupResult.found(normalizedMobile) : TestCleanupResult.notFound();
result.put("PTL_USER_INVITATION", deleted);
return result;
}
/**
* 휴대폰 번호로 찾은 계정을 법인 소속에서만 제외해 개인회원으로 되돌린다.
* 1020이 수락/소속 제외 전에 중단된 경우, 1000 선행 개인회원은 보존하면서 재초대 가능 상태로 복구한다.
*/
public TestCleanupResult detachUsersFromOrgByMobile(String mobile) {
assertNonProdProfile();
String normalizedMobile = PhoneNumberUtil.normalize(mobile);
if (normalizedMobile == null) {
return TestCleanupResult.notFound();
}
TestCleanupResult result = TestCleanupResult.notFound();
for (PortalUser user : portalUserRepository.findAllByMobileNumber(normalizedMobile)) {
if (user.getPortalOrg() == null) {
continue;
}
user.setPortalOrg(null);
user.setRoleCode(RoleCode.ROLE_USER);
portalUserRepository.save(user);
result = TestCleanupResult.found(user.getId());
result.put("PTL_USER_ORG_MEMBERSHIP", 1L);
}
return result;
}
private TestCleanupResult deleteUserCascadeInternal(PortalUser user) {
TestCleanupResult result = TestCleanupResult.found(user.getId());
result.put("PTL_INQUIRY_COMMENT", inquiryCommentRepository.deleteByInquiry_Inquirer_Id(user.getId()));