diff --git a/src/main/java/com/eactive/apim/portal/djb/testcleanup/controller/TestCleanupInternalController.java b/src/main/java/com/eactive/apim/portal/djb/testcleanup/controller/TestCleanupInternalController.java index ad1458d..c392cff 100644 --- a/src/main/java/com/eactive/apim/portal/djb/testcleanup/controller/TestCleanupInternalController.java +++ b/src/main/java/com/eactive/apim/portal/djb/testcleanup/controller/TestCleanupInternalController.java @@ -253,6 +253,63 @@ public class TestCleanupInternalController { return ResponseEntity.ok(body); } + /** + * 4010 실행 전, 지정 이메일 계정을 관리자 이메일로 조회한 법인의 ROLE_CORP_USER(법인개발자)로 준비한다. + * 이미 존재하면 재소속+비밀번호 재설정(heal), 없으면 신규 생성(create) — 두 경우 모두 응답한 비밀번호로 + * 곧바로 로그인 가능한 상태가 된다. + */ + @PostMapping("/corp-developer") + public ResponseEntity> ensureCorpDeveloper( + @RequestParam String managerEmail, @RequestParam String email, @RequestParam String password, + @RequestParam String mobile, @RequestParam String userName, HttpServletRequest request) { + ResponseEntity> guardFailure = checkGuards(request); + if (guardFailure != null) { + return guardFailure; + } + if (StringUtils.isBlank(managerEmail) || StringUtils.isBlank(email) || StringUtils.isBlank(password) + || StringUtils.isBlank(mobile) || StringUtils.isBlank(userName)) { + return badRequest("managerEmail, email, password, mobile, userName 은 모두 필수입니다."); + } + + TestCleanupResult result = testCleanupService.ensureTestCorpDeveloper(managerEmail, email, password, mobile, userName); + boolean created = result.getDeletedCounts().getOrDefault("PTL_USER_CREATED", 0L) > 0; + log.info("테스트 정리(corp-developer) 실행 - managerEmail: {}, email: {}, created: {}, from: {}", + managerEmail, email, created, request.getRemoteAddr()); + + Map body = baseBody(); + body.put("managerEmail", managerEmail); + body.put("email", email); + body.put("userId", result.getTargetId()); + body.put("created", created); + body.put("deletedCounts", result.getDeletedCounts()); + return ResponseEntity.ok(body); + } + + /** + * 테스트 계정의 비밀번호만 재설정한다(소속/역할/상태는 그대로). 화면 가입 절차 없이 즉시 로그인 + * 가능한 값으로 되돌리는 용도 — 공용 시드 계정의 비밀번호가 정책과 안 맞게 바뀐 경우 등. + */ + @PostMapping("/password") + public ResponseEntity> resetPassword( + @RequestParam String email, @RequestParam String password, HttpServletRequest request) { + ResponseEntity> guardFailure = checkGuards(request); + if (guardFailure != null) { + return guardFailure; + } + if (StringUtils.isBlank(email) || StringUtils.isBlank(password)) { + return badRequest("email 과 password 는 필수입니다."); + } + + TestCleanupResult result = testCleanupService.resetTestPassword(email, password); + log.info("테스트 정리(password) 실행 - email: {}, found: {}, from: {}", + email, result.isFound(), request.getRemoteAddr()); + + Map body = baseBody(); + body.put("email", email); + body.put("userFound", result.isFound()); + return ResponseEntity.ok(body); + } + /** * 휴대폰 번호로 남아 있는 초대 레코드를 삭제한다. 1020 재실행 전 초대중 중복을 정리하는 용도다. */ diff --git a/src/main/java/com/eactive/apim/portal/djb/testcleanup/service/TestCleanupService.java b/src/main/java/com/eactive/apim/portal/djb/testcleanup/service/TestCleanupService.java index 52eb0dc..a1b4a9e 100644 --- a/src/main/java/com/eactive/apim/portal/djb/testcleanup/service/TestCleanupService.java +++ b/src/main/java/com/eactive/apim/portal/djb/testcleanup/service/TestCleanupService.java @@ -8,7 +8,9 @@ import com.eactive.apim.portal.approval.statemachine.RequestedState; import com.eactive.apim.portal.apps.approval.service.ApprovalService; import com.eactive.apim.portal.apps.community.partnership.repository.PartnershipApplicationRepository; import com.eactive.apim.portal.apps.community.qna.repository.InquiryRepository; +import com.eactive.apim.portal.apps.user.dto.PortalUserRegistrationDTO; import com.eactive.apim.portal.apps.user.repository.PortalOrgRepository; +import com.eactive.apim.portal.apps.user.service.PortalUserService; import com.eactive.apim.portal.djb.community.qna.comment.repository.InquiryCommentRepository; import com.eactive.apim.portal.djb.webhook.repository.WebhookRequestApiRepository; import com.eactive.apim.portal.djb.webhook.repository.WebhookRequestEventRepository; @@ -21,6 +23,7 @@ import com.eactive.apim.portal.partnershipapplication.entity.PartnershipApplicat 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; 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; @@ -28,6 +31,7 @@ import com.eactive.apim.portal.portaluser.repository.UserPasswordHistoryReposito import com.eactive.apim.portal.portaluser.repository.UserRoleHistoryRepository; import com.eactive.apim.portal.qna.entity.Inquiry; import com.eactive.apim.portal.user.repository.UserLogRepository; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -36,6 +40,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Profile; import org.springframework.core.env.Environment; import org.springframework.core.env.Profiles; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -74,6 +79,8 @@ public class TestCleanupService { private final UserInvitationRepository userInvitationRepository; private final FileService fileService; private final TestCleanupNativeQueries nativeQueries; + private final PortalUserService portalUserService; + private final PasswordEncoder passwordEncoder; /** * 사업자등록번호로 법인을 찾아, 소속 계정 전원 + org 소유 CREDENTIAL/WEBHOOK + 법인 자체를 하드 삭제한다. @@ -315,6 +322,84 @@ public class TestCleanupService { return result; } + /** + * 4010 재실행 안정성을 위한 테스트 전용 API. 지정 이메일 계정을 관리자 이메일로 조회한 법인의 + * ROLE_CORP_USER(법인개발자)로 만든다 — 이미 존재하면(다른 시나리오/수작업이 소속을 해제했더라도) + * 재소속·비밀번호 재설정으로 로그인 가능 상태를 보장하고(heal), 계정이 없으면 새로 만든다(create). + * 계정 생성/소속 전환 로직은 초대 수락 경로({@link PortalUserService#registerInvitedUser}, + * {@link PortalUserService#updateUserToCorpUser})를 그대로 재사용한다. + */ + public TestCleanupResult ensureTestCorpDeveloper( + String managerEmail, String email, String password, String mobile, String userName) { + assertNonProdProfile(); + + PortalUser manager = portalUserRepository.findPortalUserByEmailAddr(managerEmail) + .orElseThrow(() -> new IllegalArgumentException("관리자 계정을 찾을 수 없습니다: " + managerEmail)); + PortalOrg org = manager.getPortalOrg(); + if (org == null) { + throw new IllegalArgumentException("관리자 계정이 법인에 소속되어 있지 않습니다: " + managerEmail); + } + + Optional existing = portalUserRepository.findPortalUserByEmailAddr(email); + boolean created; + PortalUser user; + if (existing.isPresent()) { + user = existing.get(); + portalUserService.updateUserToCorpUser(user, org.getId()); + user.setUserStatus(PortalUserEnums.UserStatus.ACTIVE); + user.setApprovalStatus(PortalUserEnums.ApprovalStatus.COMPLETED); + user.setAccountLockYn("N"); + user.setLoginFailureCount(0); + user.setPasswordHash(passwordEncoder.encode(password)); + user.setPasswordChangeDate(LocalDateTime.now()); + String normalizedMobile = PhoneNumberUtil.normalize(mobile); + if (normalizedMobile != null) { + user.setMobileNumber(normalizedMobile); + } + portalUserRepository.save(user); + created = false; + } else { + PortalUserRegistrationDTO dto = new PortalUserRegistrationDTO(); + dto.setLoginId(email); + dto.setUserName(userName); + dto.setPassword(password); + dto.setMobileNumber(mobile); + user = portalUserService.registerInvitedUser(dto, org.getId()); + created = true; + } + + TestCleanupResult result = TestCleanupResult.found(user.getId()); + result.put("PTL_USER_CREATED", created ? 1L : 0L); + result.put("PTL_USER_ATTACHED", created ? 0L : 1L); + log.info("테스트 정리 - 법인개발자 계정 준비 완료: email={}, orgId={}, created={}", email, org.getId(), created); + return result; + } + + /** + * 테스트 계정의 비밀번호만 재설정한다(소속/역할/상태는 건드리지 않음). 계정이 아예 없으면 not-found. + * 4010 등에서 재사용하는 공용 계정(예: REG_CORP_LOGIN_ID)의 비밀번호가 클라이언트 정책(길이/복잡도)에 + * 안 맞게 바뀌었을 때, 화면 가입 절차 없이 즉시 로그인 가능한 값으로 되돌리는 용도. + */ + public TestCleanupResult resetTestPassword(String email, String password) { + assertNonProdProfile(); + + Optional userOpt = portalUserRepository.findPortalUserByEmailAddr(email); + if (!userOpt.isPresent()) { + return TestCleanupResult.notFound(); + } + PortalUser user = userOpt.get(); + user.setPasswordHash(passwordEncoder.encode(password)); + user.setPasswordChangeDate(LocalDateTime.now()); + user.setLoginFailureCount(0); + user.setAccountLockYn("N"); + portalUserRepository.save(user); + + TestCleanupResult result = TestCleanupResult.found(user.getId()); + result.put("PTL_USER_PASSWORD_RESET", 1L); + log.info("테스트 정리 - 비밀번호 재설정 완료: email={}", email); + return result; + } + private TestCleanupResult deleteUserCascadeInternal(PortalUser user) { TestCleanupResult result = TestCleanupResult.found(user.getId()); result.put("PTL_INQUIRY_COMMENT", inquiryCommentRepository.deleteByInquiry_Inquirer_Id(user.getId()));