From 73deb7f02e201939a1f5fb42fedeeb9fbdb80a68 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Tue, 28 Jul 2026 11:27:58 +0900 Subject: [PATCH 1/5] =?UTF-8?q?WebLogic=20WebSocket=20=EC=B6=A9=EB=8F=8C?= =?UTF-8?q?=20=EB=B0=A9=EC=A7=80=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?:=20-=20Tomcat=20WebSocket=20=EB=AA=A8=EB=93=88(tomcat-embed-we?= =?UTF-8?q?bsocket)=20=EC=A0=9C=EC=99=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) 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 { From d2bc61dc3180aca3e8cf6edd3a86e64644ea5d05 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Tue, 28 Jul 2026 12:41:02 +0900 Subject: [PATCH 2/5] =?UTF-8?q?2FA=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=B0=8F=20=EC=86=8D=EC=84=B1=20=EC=B6=94=EA=B0=80=20-=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=AA=A8=EB=93=88=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20-=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EA=B2=BD=EB=A1=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20=EB=B0=8F=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EB=B3=80=EA=B2=BD=20-=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EA=B2=BD=EB=A1=9C=20=EB=B0=8F=20=EB=AA=A9?= =?UTF-8?q?(mock)=20=EA=B0=9D=EC=B2=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../portal/apps/user/AccountControllerTest.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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()) From b29882c0b2bbbb105955cb4bf3004b0fd3b5b88e Mon Sep 17 00:00:00 2001 From: Rinjae Date: Tue, 28 Jul 2026 15:13:55 +0900 Subject: [PATCH 3/5] =?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 서비스를 이용하기 위해서
- 이메일 인증을 완료해 주세요. -

From a2f6151a08226bfad863e2ad366544a437153ccc Mon Sep 17 00:00:00 2001 From: Rinjae Date: Tue, 28 Jul 2026 15:31:21 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B4=80=EB=A0=A8=20P?= =?UTF-8?q?TL=5FPROPERTY=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80:?= =?UTF-8?q?=20-=20=EA=B0=9C=EB=B0=9C/=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EC=9D=B8=EC=A6=9D=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EB=85=B8=EC=B6=9C=20=EC=84=A4=EC=A0=95=20AuthNoticeProperties?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20-=20=EC=82=AD=EC=A0=9C=EB=90=9C=20?= =?UTF-8?q?=EB=B2=95=EC=9D=B8=20=EC=86=8C=EC=86=8D=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=B0=A8=EB=8B=A8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apps/auth/AuthNoticeProperties.java | 43 +++++++++++++++++++ .../config/PortalAuthenticationManager.java | 6 +++ 2 files changed, 49 insertions(+) create mode 100644 src/main/java/com/eactive/apim/portal/apps/auth/AuthNoticeProperties.java 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/config/PortalAuthenticationManager.java b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationManager.java index d8d65b5..135ec29 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,12 @@ public class PortalAuthenticationManager implements AuthenticationManager { } if (user.getPortalOrg() != null) { + // 삭제(탈퇴)된 법인 소속 사용자는 2FA 이전에 로그인 차단 + if (PortalOrgEnums.OrgStatus.REMOVED.equals(user.getPortalOrg().getOrgStatus())) { + log.debug("삭제된 법인 소속 로그인 차단 - user : {} / org.orgStatus : {}", user.getUsername(), user.getPortalOrg().getOrgStatus()); + throw new DisabledException("삭제된 법인의 계정입니다. 로그인할 수 없습니다."); + } + if (!user.getPortalOrg().getApprovalStatus().equals(PortalOrgEnums.ApprovalStatus.COMPLETED)) { log.debug("기업사용자 - getApprovalStatus : {} / - org.approvalStatus : {}", user.getApprovalStatus(), user.getPortalOrg().getApprovalStatus()); throw new DisabledException("로그인할 수 없습니다. 관리자에게 문의하세요. (법인 승인대기중)"); From 87369ce7f786aa3bdb14424049997f1a7c020bb7 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Tue, 28 Jul 2026 15:38:18 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=EB=B2=95=EC=9D=B8=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=B0=A8=EB=8B=A8=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0:=20-=20ACTIVE=20=EC=99=B8=20=EC=83=81=ED=83=9C(?= =?UTF-8?q?=ED=9C=B4=EB=A9=B4/=ED=83=88=ED=87=B4/=EC=A4=80=EB=B9=84)=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=A0=9C=ED=95=9C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80=20-=20=EC=8A=B9=EC=9D=B8=20?= =?UTF-8?q?=EB=AF=B8=EC=99=84=EB=A3=8C=20=EC=98=88=EC=99=B8=20=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EB=B0=A9=EC=96=B4=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/PortalAuthenticationManager.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) 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 135ec29..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,12 +79,21 @@ public class PortalAuthenticationManager implements AuthenticationManager { } if (user.getPortalOrg() != null) { - // 삭제(탈퇴)된 법인 소속 사용자는 2FA 이전에 로그인 차단 - if (PortalOrgEnums.OrgStatus.REMOVED.equals(user.getPortalOrg().getOrgStatus())) { - log.debug("삭제된 법인 소속 로그인 차단 - user : {} / org.orgStatus : {}", user.getUsername(), user.getPortalOrg().getOrgStatus()); - throw new DisabledException("삭제된 법인의 계정입니다. 로그인할 수 없습니다."); + // 법인은 "정상(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("로그인할 수 없습니다. 관리자에게 문의하세요. (법인 승인대기중)");