Merge branch 'master' into design

This commit is contained in:
Rinjae
2026-07-28 16:07:51 +09:00
20 changed files with 186 additions and 32 deletions
+8
View File
@@ -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 {
@@ -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 접근 래퍼.
*
* <p>그룹 {@code Portal}, 키 {@code auth.test-notice.enabled}(true/false). 값이 참이면 인증 요청
* 응답에 인증번호를 실어 화면에 노출한다(실제 발송 대신 테스트 확인 용도). 기존 application.yml
* {@code portal.test-auth-notice-enabled} 설정을 DB PTL_PROPERTY 로 이전한 것으로,
* {@link TwoFactorProperties} 와 동일한 {@code getOrCreateProperty} 패턴을 따른다.</p>
*
* <p><b>prod 프로파일에서는 DB 값과 무관하게 항상 false</b> 를 반환한다(운영 환경 인증번호 노출 금지).
* 세션 keepalive 등 다른 비운영 전용 스위치와 동일한 정책이다.</p>
*/
@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());
}
}
@@ -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(
@@ -146,12 +146,13 @@ public class UserRegisterController {
if (invitationToken == null) {
Optional<PortalUser> 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) → 회원가입 직후 이메일 인증 단계로 이동(기존 흐름 유지)
@@ -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());
@@ -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(
@@ -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);
@@ -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<String, String> 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);
@@ -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";
@@ -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("로그인할 수 없습니다. 관리자에게 문의하세요. (법인 승인대기중)");
@@ -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 등)가 쿼리스트링으로 노출되는 것을 차단한다.
*
* <p>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/기존 설정은 그대로 유지) 누수를 막는다.</p>
*
* <p>{@code static} 메서드로 선언해 이 설정 클래스가 조기 초기화되는 것을 피한다.
* 명시적 {@code RedirectAttributes}/flash 속성은 영향받지 않는다(기본 모델만 무시).</p>
*/
@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 추가
// -------------------------------------------------------------
@@ -25,8 +25,6 @@ public class PortalProperties {
private String authVirtualCode = "";
private boolean testAuthNoticeEnabled = false;
private Map<RoleCode, List<String>> portalSecurity;
private FileProperties file = new FileProperties();
-1
View File
@@ -40,7 +40,6 @@ gateway:
portal:
# auth-virtual-code: 654321
test-auth-notice-enabled: true
dev:
# application.yml의 `page:` 트리(브레드크럼/메뉴 이름) 라이브 반영
hot-reload-pages: true
-3
View File
@@ -28,9 +28,6 @@ spring:
thymeleaf:
cache: false
portal:
test-auth-notice-enabled: false # prod 환경: UI 안내 미표시
app:
resource-versioning:
enabled: true
-1
View File
@@ -22,7 +22,6 @@ spring:
cache: false
portal:
test-auth-notice-enabled: true
# 검증 단계에선 사용하지 않음
# auth-virtual-code: 654321
dev:
-1
View File
@@ -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
@@ -54,6 +54,8 @@
</div>
<button type="button" class="btn org-btn-check" id="btnVerifyEmailCode">인증확인</button>
</div>
<div id="emailTestNotice" class="test-env-notice"
style="display: none; margin-top: 8px; padding: 10px 12px; background-color: #FFF4E6; border: 1px solid #FFB84D; border-radius: 4px; font-size: 13px; color: #5D4037; line-height: 1.5;"></div>
<input type="hidden" id="emailVerified" name="emailVerified" value="false"/>
<div id="email-auth-validation" class="org-validation-message"></div>
</div>
@@ -240,6 +242,12 @@
setEmailAuthMsg('인증번호를 발송했습니다.', false);
btn.text('인증번호 재발송');
startEmailCodeTimer();
// 테스트 환경(PTL_PROPERTY auth.test-notice.enabled)일 때만 인증번호 노출
if (res.authNumber) {
$('#emailTestNotice').html('테스트 환경입니다. 실제 이메일이 발송되지 않으며, 인증번호는 <strong style="color:#E65100; font-family:monospace; font-size:14px;">' + res.authNumber + '</strong> 입니다.').show();
} else {
$('#emailTestNotice').hide().empty();
}
} else {
setEmailAuthMsg((res && res.message) || '발송에 실패했습니다.', true);
}
@@ -45,6 +45,8 @@
</div>
<button type="button" class="btn org-btn-check" id="btnVerifyEmailCode">인증확인</button>
</div>
<div id="emailTestNotice" class="test-env-notice"
style="display: none; margin-top: 8px; padding: 10px 12px; background-color: #FFF4E6; border: 1px solid #FFB84D; border-radius: 4px; font-size: 13px; color: #5D4037; line-height: 1.5;"></div>
<input type="hidden" id="emailVerified" name="emailVerified" value="false"/>
<div id="email-auth-validation" class="org-validation-message"></div>
</div>
@@ -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 = '테스트 환경입니다. 실제 이메일이 발송되지 않으며, 인증번호는 <strong style="color:#E65100; font-family:monospace; font-size:14px;">' + res.authNumber + '</strong> 입니다.';
notice.style.display = '';
} else {
notice.style.display = 'none';
notice.innerHTML = '';
}
}
} else {
setEmailAuthMsg((res && res.message) || '발송에 실패했습니다.', true);
}
@@ -24,19 +24,15 @@
<image id="image0_1130_6799" width="163" height="129" preserveAspectRatio="none" xlink:href="data:image/png;base64,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"/>
</defs>
</svg>
<h2 class="result-title" th:text="${registrationType == 'corporate'} ? '법인 신청이 완료 되었습니다.' : '회원가입 신청이 완료되었습니다.'">회원가입 신청이 완료되었습니다.</h2>
<h2 class="result-title" th:text="${registrationType == 'corporate'} ? '법인 신청이 완료 되었습니다.' : '회원가입이 완료되었습니다.'">회원가입이 완료되었습니다.</h2>
</div>
<!-- Info Box -->
<div class="result-info-box">
<p class="info-text" th:if="${registrationType == 'corporate'}">
<div class="result-info-box" th:if="${registrationType == 'corporate'}">
<p class="info-text">
관리자의 확인 및 승인 이후<br>
이용하실 수 있습니다.
</p>
<p class="info-text" th:unless="${registrationType == 'corporate'}">
API Portal 서비스를 이용하기 위해서<br>
<strong>이메일 인증</strong>을 완료해 주세요.
</p>
</div>
</div>
@@ -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> 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())