From b29882c0b2bbbb105955cb4bf3004b0fd3b5b88e Mon Sep 17 00:00:00 2001 From: Rinjae Date: Tue, 28 Jul 2026 15:13:55 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=ED=9D=90=EB=A6=84=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=99=98=EA=B2=BD=20=EB=8C=80?= =?UTF-8?q?=EC=9D=91:=20-=20=EA=B0=80=EC=9E=85=20=EC=8B=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?-=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=99=98=EA=B2=BD=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=9D=B8=EC=A6=9D=EB=B2=88=ED=98=B8=20UI=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=EB=B0=8F=20=EA=B4=80=EB=A6=AC=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20-=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8?= =?UTF-8?q?=20=EC=8B=9C=20=EA=B8=B0=EB=B3=B8=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EB=85=B8=EC=B6=9C=20=EB=B0=A9=EC=A7=80=20Bean=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/OrgRegisterController.java | 17 +++++++++-- .../controller/UserRegisterController.java | 5 ++-- .../apps/user/facade/AuthFacadeImpl.java | 7 ++++- .../apps/user/facade/OrgRegisterFacade.java | 5 ++-- .../user/facade/OrgRegisterFacadeImpl.java | 10 ++++--- .../apps/user/service/PortalUserService.java | 15 ++++++++++ .../breadcrumb/GlobalControllerAdvice.java | 8 +++-- .../PortalConfigWebDispatcherServlet.java | 29 +++++++++++++++++++ .../apim/portal/config/PortalProperties.java | 2 -- src/main/resources/application-dev.yml | 1 - src/main/resources/application-prod.yml | 3 -- src/main/resources/application-stage.yml | 1 - src/main/resources/application.yml | 1 - .../components/commonUserInfoForm.html | 8 +++++ .../register/components/orgUserInfoForm.html | 16 +++++++++- .../apps/register/userRegisterResult.html | 10 ++----- 16 files changed, 109 insertions(+), 29 deletions(-) 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/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 서비스를 이용하기 위해서
- 이메일 인증을 완료해 주세요. -