diff --git a/build.gradle b/build.gradle index ff330c4..90b7ea6 100644 --- a/build.gradle +++ b/build.gradle @@ -145,6 +145,14 @@ sourceSets { configurations { annotationProcessor + + // WebLogic 배포 시 Tyrus WebSocket 필터(weblogic.websocket.tyrus.TyrusServletFilter)와 + // 충돌 방지: WAR 에 번들된 Tomcat WsSci 가 javax.websocket.server.ServerContainer 속성을 + // WsServerContainer 로 등록 → WebLogic Tyrus 필터가 TyrusServerContainer 로 캐스팅하다 실패. + // 앱은 WebSocket 미사용이므로 Tomcat WebSocket 모듈 제외. + all { + exclude group: 'org.apache.tomcat.embed', module: 'tomcat-embed-websocket' + } } compileJava { diff --git a/src/main/java/com/eactive/apim/portal/apps/auth/AuthNoticeProperties.java b/src/main/java/com/eactive/apim/portal/apps/auth/AuthNoticeProperties.java new file mode 100644 index 0000000..44d2c26 --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/apps/auth/AuthNoticeProperties.java @@ -0,0 +1,43 @@ +package com.eactive.apim.portal.apps.auth; + +import com.eactive.apim.portal.portalproperty.service.PortalPropertyService; +import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; +import org.springframework.stereotype.Component; + +/** + * 인증(이메일/SMS) 테스트 안내 관련 PTL_PROPERTY 접근 래퍼. + * + *

그룹 {@code Portal}, 키 {@code auth.test-notice.enabled}(true/false). 값이 참이면 인증 요청 + * 응답에 인증번호를 실어 화면에 노출한다(실제 발송 대신 테스트 확인 용도). 기존 application.yml + * {@code portal.test-auth-notice-enabled} 설정을 DB PTL_PROPERTY 로 이전한 것으로, + * {@link TwoFactorProperties} 와 동일한 {@code getOrCreateProperty} 패턴을 따른다.

+ * + *

prod 프로파일에서는 DB 값과 무관하게 항상 false 를 반환한다(운영 환경 인증번호 노출 금지). + * 세션 keepalive 등 다른 비운영 전용 스위치와 동일한 정책이다.

+ */ +@Component +@RequiredArgsConstructor +public class AuthNoticeProperties { + + public static final String GROUP = "Portal"; + public static final String KEY_TEST_NOTICE_ENABLED = "auth.test-notice.enabled"; + + private final PortalPropertyService portalPropertyService; + private final Environment environment; + + /** + * 인증 요청 응답에 인증번호를 실어 UI 에 노출할지 여부(개발/테스트 전용). + * prod 환경에서는 property 값과 무관하게 항상 false. + */ + public boolean isTestNoticeEnabled() { + if (environment.acceptsProfiles(Profiles.of("prod"))) { + return false; + } + String value = portalPropertyService.getOrCreateProperty( + GROUP, KEY_TEST_NOTICE_ENABLED, "true", + "인증(이메일/SMS) 요청 시 인증번호를 화면에 표시할지 여부 (true/false, 테스트 전용)"); + return value != null && "true".equalsIgnoreCase(value.trim()); + } +} diff --git a/src/main/java/com/eactive/apim/portal/apps/user/controller/OrgRegisterController.java b/src/main/java/com/eactive/apim/portal/apps/user/controller/OrgRegisterController.java index 2fbe817..f58521c 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/controller/OrgRegisterController.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/controller/OrgRegisterController.java @@ -11,6 +11,8 @@ import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpSession; + @Controller @RequestMapping("/signup/portalOrg") @RequiredArgsConstructor @@ -39,17 +41,28 @@ public class OrgRegisterController { @RequestParam("registrationScenario") String scenario, @ModelAttribute PortalOrgRegistrationDTO orgDTO, @ModelAttribute UserAgreementDTO agreementDTO, + HttpSession session, Model model) { try { ValidationResponse response; switch (scenario) { - case "new": + case "new": { + // 가입 폼에서 이메일 인증을 마쳤는지 세션으로 검증 (개인 경로와 동일, 클라 hidden 불신) + String verifiedEmail = (String) session.getAttribute("signupVerifiedEmail"); + boolean emailVerified = verifiedEmail != null + && verifiedEmail.equalsIgnoreCase(orgDTO.getLoginId()); response = orgRegisterFacade.registerNewOrgUser( orgDTO, - agreementDTO); + agreementDTO, + emailVerified); + if (response.isValid() && emailVerified) { + session.removeAttribute("signupVerifiedEmail"); + session.removeAttribute("signupEmailPending"); + } break; + } case "retain": response = orgRegisterFacade.convertToOrgUser( diff --git a/src/main/java/com/eactive/apim/portal/apps/user/controller/UserRegisterController.java b/src/main/java/com/eactive/apim/portal/apps/user/controller/UserRegisterController.java index b44e8a4..f3cd2d8 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/controller/UserRegisterController.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/controller/UserRegisterController.java @@ -146,12 +146,13 @@ public class UserRegisterController { if (invitationToken == null) { Optional registered = portalUserService.findByLoginId(portalUserRegistrationDTO.getLoginId()); - // 이메일 인증을 마쳐 ACTIVE 로 저장된 경우 → 바로 자동 로그인(2차 인증 없이) 후 메인 이동 + // 이메일 인증을 마쳐 ACTIVE 로 저장된 경우 → 자동 로그인(2차 인증 없이) 후 가입 완료 페이지 노출 + // (완료 페이지의 '홈으로' 버튼으로 로그인 상태 그대로 메인 이동) if (registered.isPresent() && PortalUserEnums.UserStatus.ACTIVE.equals(registered.get().getUserStatus())) { portalUserAuthService.autoLoginAfterSignup(registered.get(), request); redirectAttributes.addFlashAttribute("message", "회원가입이 완료되었습니다."); - return "redirect:/"; + return "redirect:/signup/complete"; } // 이메일 미인증(READY) → 회원가입 직후 이메일 인증 단계로 이동(기존 흐름 유지) diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/AuthFacadeImpl.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/AuthFacadeImpl.java index 371f87d..b4ec6b1 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/facade/AuthFacadeImpl.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/AuthFacadeImpl.java @@ -1,5 +1,6 @@ package com.eactive.apim.portal.apps.user.facade; +import com.eactive.apim.portal.apps.auth.AuthNoticeProperties; import com.eactive.apim.portal.apps.auth.service.AuthNumberService; import com.eactive.apim.portal.apps.user.dto.ValidationResponse; import lombok.RequiredArgsConstructor; @@ -13,6 +14,7 @@ import org.slf4j.LoggerFactory; public class AuthFacadeImpl implements AuthFacade { private final AuthNumberService authNumberService; + private final AuthNoticeProperties authNoticeProperties; private static final Logger log = LoggerFactory.getLogger(AuthFacadeImpl.class); @@ -44,7 +46,10 @@ public class AuthFacadeImpl implements AuthFacade { String generatedAuthNumber = authNumberService.sendRequestAuthNumber(recipientKey, msgType); response.setValid(true); response.setMessage("인증번호를 발송하였습니다."); - response.setAuthNumber(generatedAuthNumber); // 테스트 환경에서 인증번호 표시용 + // 테스트 환경(PTL_PROPERTY auth.test-notice.enabled=true, prod 제외)에서만 인증번호를 응답에 노출 + if (authNoticeProperties.isTestNoticeEnabled()) { + response.setAuthNumber(generatedAuthNumber); + } } catch (Exception e) { response.setValid(false); response.setMessage(e.getMessage()); diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/OrgRegisterFacade.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/OrgRegisterFacade.java index b7ac80a..e916f8c 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/facade/OrgRegisterFacade.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/OrgRegisterFacade.java @@ -7,10 +7,11 @@ import org.springframework.http.ResponseEntity; public interface OrgRegisterFacade { - // 신규 법인 회원 등록 + // 신규 법인 회원 등록 (emailVerified: 가입 폼에서 이메일 인증 완료 시 true → 바로 ACTIVE) ValidationResponse registerNewOrgUser( PortalOrgRegistrationDTO orgDTO, - UserAgreementDTO agreementDTO) ; + UserAgreementDTO agreementDTO, + boolean emailVerified) ; // 기존 회원의 법인 전환 ValidationResponse convertToOrgUser( diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/OrgRegisterFacadeImpl.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/OrgRegisterFacadeImpl.java index f79b354..6c5f77c 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/facade/OrgRegisterFacadeImpl.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/OrgRegisterFacadeImpl.java @@ -64,7 +64,8 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade { @Transactional public ValidationResponse registerNewOrgUser( PortalOrgRegistrationDTO orgDTO, - UserAgreementDTO agreementDTO) { + UserAgreementDTO agreementDTO, + boolean emailVerified) { if (!agreementValidator.isAgreementAccepted(agreementDTO)) { return new ValidationResponse(false, "약관에 모두 동의해야 합니다."); @@ -87,7 +88,7 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade { if (uploadedFile == null) { return new ValidationResponse(false, "첨부파일이 올바르지 않습니다."); } - return registerNewCorporateUser(orgDTO, uploadedFile); + return registerNewCorporateUser(orgDTO, uploadedFile, emailVerified); } catch (IllegalArgumentException | IOException e) { return new ValidationResponse(false, e.getMessage()); } @@ -179,7 +180,8 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade { // 신규 법인 사용자 등록 메서드 private ValidationResponse registerNewCorporateUser( PortalOrgRegistrationDTO orgDTO, - FileInfo uploadedFile) { + FileInfo uploadedFile, + boolean emailVerified) { // 사업자등록번호 중복 체크 (이중 방어) if (portalOrgService.existsByCompRegNo(orgDTO.getCompRegNo())) { @@ -190,7 +192,7 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade { PortalOrg newOrg = portalOrgService.registerOrgFromDTOWithFile(orgDTO, uploadedFile); // 사용자 생성 및 기관 연결 - PortalUser newUser = portalUserService.createUserWithOrg(orgDTO, newOrg, "corporate"); + PortalUser newUser = portalUserService.createUserWithOrg(orgDTO, newOrg, "corporate", emailVerified); agreementsFacade.saveUserAgreements(newUser.getId(), AgreementType.PRIVACY_COLLECT); diff --git a/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserService.java b/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserService.java index 986dd69..7dede1b 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserService.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserService.java @@ -175,11 +175,26 @@ public class PortalUserService { } public PortalUser createUserWithOrg(PortalUserRegistrationDTO userDTO, PortalOrg org, String registrationType) { + return createUserWithOrg(userDTO, org, registrationType, false); + } + + /** + * @param emailVerified 가입 폼에서 이메일 인증을 이미 완료했으면 true → 바로 ACTIVE 로 저장 + * (개인 경로 registerActiveUser 와 동일. 법인 신규가입 경로에서 사용) + */ + public PortalUser createUserWithOrg(PortalUserRegistrationDTO userDTO, PortalOrg org, String registrationType, boolean emailVerified) { PortalUser newUser = new PortalUser(); mapDtoToEntity(newUser, userDTO); newUser.setPortalOrg(org); setUserProperties(newUser); newUser.setUserStatus(UserStatus.READY); + + // 이메일 인증 기능 비활성화 시, 또는 가입 폼에서 이미 인증을 마친 경우 바로 활성화 처리 + Map propertyMap = portalPropertyService.getPortalPropertiesAsMap("Portal"); + if (emailVerified + || "true".equalsIgnoreCase(propertyMap.getOrDefault("disable_features.user_email_verify", ""))) { + newUser.setUserStatus(UserStatus.ACTIVE); + } newUser.setApprovalStatus(ApprovalStatus.COMPLETED); setUserRole(newUser, registrationType); return portalUserRepository.save(newUser); diff --git a/src/main/java/com/eactive/apim/portal/common/breadcrumb/GlobalControllerAdvice.java b/src/main/java/com/eactive/apim/portal/common/breadcrumb/GlobalControllerAdvice.java index 6f9ab52..933992b 100644 --- a/src/main/java/com/eactive/apim/portal/common/breadcrumb/GlobalControllerAdvice.java +++ b/src/main/java/com/eactive/apim/portal/common/breadcrumb/GlobalControllerAdvice.java @@ -9,6 +9,7 @@ import org.springframework.core.env.Profiles; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ModelAttribute; import com.eactive.apim.portal.config.PortalProperties; +import com.eactive.apim.portal.apps.auth.AuthNoticeProperties; import com.eactive.apim.portal.apps.session.service.UserSessionService; import com.eactive.apim.portal.common.security.ClientGuardService; import com.eactive.apim.portal.portalproperty.service.PortalPropertyService; @@ -31,6 +32,9 @@ public class GlobalControllerAdvice { @Autowired private PortalPropertyService portalPropertyService; + @Autowired + private AuthNoticeProperties authNoticeProperties; + @Autowired private Environment environment; @@ -48,12 +52,12 @@ public class GlobalControllerAdvice { @ModelAttribute("showTestAuthNotice") public boolean showTestAuthNotice() { - return portalProperties.isTestAuthNoticeEnabled(); + return authNoticeProperties.isTestNoticeEnabled(); } @ModelAttribute("testAuthNumber") public String getTestAuthNumber() { - if (portalProperties.isTestAuthNoticeEnabled()) { + if (authNoticeProperties.isTestNoticeEnabled()) { String authVirtualCode = portalProperties.getAuthVirtualCode(); // 고정 인증번호가 있으면 반환, 없으면 "random" 표시 return (authVirtualCode != null && !authVirtualCode.isEmpty()) ? authVirtualCode : "random"; diff --git a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationManager.java b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationManager.java index d8d65b5..f4eb214 100644 --- a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationManager.java +++ b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationManager.java @@ -79,6 +79,21 @@ public class PortalAuthenticationManager implements AuthenticationManager { } if (user.getPortalOrg() != null) { + // 법인은 "정상(ACTIVE)" 상태에서만 로그인 허용 (준비/탈퇴/휴면 차단, 2FA 이전 단계) + PortalOrgEnums.OrgStatus orgStatus = user.getPortalOrg().getOrgStatus(); + if (!PortalOrgEnums.OrgStatus.ACTIVE.equals(orgStatus)) { + log.debug("법인 비정상 상태 로그인 차단 - user : {} / org.orgStatus : {}", user.getUsername(), orgStatus); + if (PortalOrgEnums.OrgStatus.REMOVED.equals(orgStatus)) { + throw new DisabledException("삭제된 법인의 계정입니다. 로그인할 수 없습니다."); + } + if (PortalOrgEnums.OrgStatus.INACTIVE.equals(orgStatus)) { + throw new DisabledException("휴면 상태의 법인입니다. 관리자에게 문의하세요."); + } + // READY 등 승인 전 상태 + throw new DisabledException("로그인할 수 없습니다. 관리자에게 문의하세요. (법인 승인대기중)"); + } + + // 방어적: ACTIVE 인데 승인 미완료인 예외 케이스 차단 if (!user.getPortalOrg().getApprovalStatus().equals(PortalOrgEnums.ApprovalStatus.COMPLETED)) { log.debug("기업사용자 - getApprovalStatus : {} / - org.approvalStatus : {}", user.getApprovalStatus(), user.getPortalOrg().getApprovalStatus()); throw new DisabledException("로그인할 수 없습니다. 관리자에게 문의하세요. (법인 승인대기중)"); diff --git a/src/main/java/com/eactive/apim/portal/config/PortalConfigWebDispatcherServlet.java b/src/main/java/com/eactive/apim/portal/config/PortalConfigWebDispatcherServlet.java index 4080179..503d534 100644 --- a/src/main/java/com/eactive/apim/portal/config/PortalConfigWebDispatcherServlet.java +++ b/src/main/java/com/eactive/apim/portal/config/PortalConfigWebDispatcherServlet.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; import nz.net.ultraq.thymeleaf.layoutdialect.LayoutDialect; import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletComponentScan; import org.springframework.context.annotation.Bean; @@ -26,6 +27,7 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.resource.PathResourceResolver; import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter; import org.springframework.web.servlet.resource.VersionResourceResolver; @@ -68,6 +70,33 @@ public class PortalConfigWebDispatcherServlet implements WebMvcConfigurer { return new LayoutDialect(); } + /** + * redirect 응답에 전역 {@code @ModelAttribute}(브레드크럼용 pageName·showTestAuthNotice· + * testAuthNumber·sessionTimeoutMinutes 등)가 쿼리스트링으로 노출되는 것을 차단한다. + * + *

Spring Boot 2.6+ 에서 {@code spring.mvc.ignore-default-model-on-redirect} 프로퍼티가 + * 제거됐고, 본 애플리케이션은 {@code @EnableWebMvc} 로 Boot 자동설정을 우회하므로 + * {@link RequestMappingHandlerAdapter} 의 프레임워크 기본값(false)이 적용된다. 그 결과 + * {@code redirect:} 를 반환하는 모든 컨트롤러에서 {@link com.eactive.apim.portal.common.breadcrumb.GlobalControllerAdvice} + * 의 기본 모델 값이 redirect URL 에 append 되어 프론트 전용 파라미터가 주소창에 그대로 드러난다. + * 어댑터 생성 이후 플래그만 true 로 뒤집어(Boot/기존 설정은 그대로 유지) 누수를 막는다.

+ * + *

{@code static} 메서드로 선언해 이 설정 클래스가 조기 초기화되는 것을 피한다. + * 명시적 {@code RedirectAttributes}/flash 속성은 영향받지 않는다(기본 모델만 무시).

+ */ + @Bean + public static BeanPostProcessor ignoreDefaultModelOnRedirectPostProcessor() { + return new BeanPostProcessor() { + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof RequestMappingHandlerAdapter) { + ((RequestMappingHandlerAdapter) bean).setIgnoreDefaultModelOnRedirect(true); + } + return bean; + } + }; + } + // ------------------------------------------------------------- // RequestMappingHandlerMapping 설정 View Controller 추가 // ------------------------------------------------------------- diff --git a/src/main/java/com/eactive/apim/portal/config/PortalProperties.java b/src/main/java/com/eactive/apim/portal/config/PortalProperties.java index f1306b5..148f79c 100644 --- a/src/main/java/com/eactive/apim/portal/config/PortalProperties.java +++ b/src/main/java/com/eactive/apim/portal/config/PortalProperties.java @@ -25,8 +25,6 @@ public class PortalProperties { private String authVirtualCode = ""; - private boolean testAuthNoticeEnabled = false; - private Map> portalSecurity; private FileProperties file = new FileProperties(); diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 88ae6a1..9b819f8 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -40,7 +40,6 @@ gateway: portal: # auth-virtual-code: 654321 - test-auth-notice-enabled: true dev: # application.yml의 `page:` 트리(브레드크럼/메뉴 이름) 라이브 반영 hot-reload-pages: true \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 6a5d06f..9980d52 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -28,9 +28,6 @@ spring: thymeleaf: cache: false -portal: - test-auth-notice-enabled: false # prod 환경: UI 안내 미표시 - app: resource-versioning: enabled: true diff --git a/src/main/resources/application-stage.yml b/src/main/resources/application-stage.yml index 79480af..511c4b5 100644 --- a/src/main/resources/application-stage.yml +++ b/src/main/resources/application-stage.yml @@ -22,7 +22,6 @@ spring: cache: false portal: - test-auth-notice-enabled: true # 검증 단계에선 사용하지 않음 # auth-virtual-code: 654321 dev: diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3ce1a25..1d1d712 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -112,7 +112,6 @@ portal: auth-ttl: 300 auth: resend_limit_seconds: 30 - test-auth-notice-enabled: true # 기본값: true (대부분의 개발환경에서 UI 안내 표시) user-approval: true password-expiration-days: 90 diff --git a/src/main/resources/templates/views/apps/register/components/commonUserInfoForm.html b/src/main/resources/templates/views/apps/register/components/commonUserInfoForm.html index 400aab4..c629d25 100644 --- a/src/main/resources/templates/views/apps/register/components/commonUserInfoForm.html +++ b/src/main/resources/templates/views/apps/register/components/commonUserInfoForm.html @@ -54,6 +54,8 @@ +
@@ -240,6 +242,12 @@ setEmailAuthMsg('인증번호를 발송했습니다.', false); btn.text('인증번호 재발송'); startEmailCodeTimer(); + // 테스트 환경(PTL_PROPERTY auth.test-notice.enabled)일 때만 인증번호 노출 + if (res.authNumber) { + $('#emailTestNotice').html('테스트 환경입니다. 실제 이메일이 발송되지 않으며, 인증번호는 ' + res.authNumber + ' 입니다.').show(); + } else { + $('#emailTestNotice').hide().empty(); + } } else { setEmailAuthMsg((res && res.message) || '발송에 실패했습니다.', true); } diff --git a/src/main/resources/templates/views/apps/register/components/orgUserInfoForm.html b/src/main/resources/templates/views/apps/register/components/orgUserInfoForm.html index e2195a7..1234580 100644 --- a/src/main/resources/templates/views/apps/register/components/orgUserInfoForm.html +++ b/src/main/resources/templates/views/apps/register/components/orgUserInfoForm.html @@ -45,6 +45,8 @@ +
@@ -122,8 +124,9 @@ if (emailChangeForm) emailChangeForm.style.display = 'none'; // 중복체크 통과 → 이메일 인증 UI 노출 + // (block 지정 시 .org-form-group 의 display:flex 를 덮어써 라벨/입력이 세로로 깨짐 → '' 로 CSS flex 복원) var evRow = document.getElementById('emailVerifyRow'); - if (evRow) evRow.style.display = 'block'; + if (evRow) evRow.style.display = ''; break; case "conversionOrChange": @@ -296,6 +299,17 @@ setEmailAuthMsg('인증번호를 발송했습니다.', false); btn.textContent = '인증번호 재발송'; startEmailCodeTimer(); + // 테스트 환경(PTL_PROPERTY auth.test-notice.enabled)일 때만 인증번호 노출 + var notice = document.getElementById('emailTestNotice'); + if (notice) { + if (res.authNumber) { + notice.innerHTML = '테스트 환경입니다. 실제 이메일이 발송되지 않으며, 인증번호는 ' + res.authNumber + ' 입니다.'; + notice.style.display = ''; + } else { + notice.style.display = 'none'; + notice.innerHTML = ''; + } + } } else { setEmailAuthMsg((res && res.message) || '발송에 실패했습니다.', true); } diff --git a/src/main/resources/templates/views/apps/register/userRegisterResult.html b/src/main/resources/templates/views/apps/register/userRegisterResult.html index 6010a6c..fd2848c 100644 --- a/src/main/resources/templates/views/apps/register/userRegisterResult.html +++ b/src/main/resources/templates/views/apps/register/userRegisterResult.html @@ -24,19 +24,15 @@ -

회원가입 신청이 완료되었습니다.

+

회원가입이 완료되었습니다.

-
-

+

+

관리자의 확인 및 승인 이후
이용하실 수 있습니다.

-

- API Portal 서비스를 이용하기 위해서
- 이메일 인증을 완료해 주세요. -

diff --git a/src/test/java/com/eactive/apim/portal/apps/user/AccountControllerTest.java b/src/test/java/com/eactive/apim/portal/apps/user/AccountControllerTest.java index c60ffbf..c5f6a2f 100644 --- a/src/test/java/com/eactive/apim/portal/apps/user/AccountControllerTest.java +++ b/src/test/java/com/eactive/apim/portal/apps/user/AccountControllerTest.java @@ -2,6 +2,8 @@ package com.eactive.apim.portal.apps.user; import com.eactive.apim.portal.apps.agreements.service.AgreementsFacade; +import com.eactive.apim.portal.apps.auth.twofactor.TwoFactorProperties; +import com.eactive.apim.portal.apps.auth.twofactor.TwoFactorService; import com.eactive.apim.portal.apps.session.service.UserSessionService; import com.eactive.apim.portal.apps.user.controller.AccountController; import com.eactive.apim.portal.apps.user.dto.PortalUserDTO; @@ -61,6 +63,12 @@ class AccountControllerTest { @Mock private UserSessionService userSessionService; + @Mock + private TwoFactorService twoFactorService; + + @Mock + private TwoFactorProperties twoFactorProperties; + @InjectMocks private AccountController accountController; @@ -73,7 +81,7 @@ class AccountControllerTest { @Test void testShowChangePasswordPage() throws Exception { - mockMvc.perform(get("/change_password")) + mockMvc.perform(get("/password/verify")) .andExpect(status().isOk()) .andExpect(view().name("apps/mypage/passwordChangeEntry")) .andExpect(model().attributeExists("passwordChangeRequest")); @@ -84,7 +92,7 @@ class AccountControllerTest { try (MockedStatic securityUtil = mockStatic(SecurityUtil.class)) { securityUtil.when(SecurityUtil::getCurrentLoginId).thenReturn("testUser"); - mockMvc.perform(post("/mypage/change_new_password") + mockMvc.perform(post("/password/change") .param("newPassword", "NewPass!1") .param("confirmPassword", "NewPass!1")) .andExpect(status().is3xxRedirection()) @@ -99,7 +107,7 @@ class AccountControllerTest { doThrow(new IllegalArgumentException("invalid password")) .when(userFacade).updatePassword(eq("testUser"), anyString(), anyString()); - mockMvc.perform(post("/mypage/change_new_password") + mockMvc.perform(post("/password/change") .param("newPassword", "NewPass!1") .param("confirmPassword", "Different!1")) .andExpect(status().isOk())