diff --git a/src/main/java/com/eactive/apim/portal/apps/session/repository/UserSessionRepository.java b/src/main/java/com/eactive/apim/portal/apps/session/repository/UserSessionRepository.java
index 5ae002e..71cc206 100644
--- a/src/main/java/com/eactive/apim/portal/apps/session/repository/UserSessionRepository.java
+++ b/src/main/java/com/eactive/apim/portal/apps/session/repository/UserSessionRepository.java
@@ -19,6 +19,10 @@ public interface UserSessionRepository extends JpaRepository :currentSessionId")
int forceLogoutOtherSessions(@Param("loginId") String loginId, @Param("currentSessionId") String currentSessionId);
+ @Modifying
+ @Query("UPDATE UserSession s SET s.forceLogout = 'Y' WHERE s.loginId = :loginId")
+ int forceLogoutAllSessions(@Param("loginId") String loginId);
+
@Modifying
@Query("DELETE FROM UserSession s WHERE s.lastAccessTime < :cutoffTime")
int deleteExpiredSessions(@Param("cutoffTime") LocalDateTime cutoffTime);
diff --git a/src/main/java/com/eactive/apim/portal/apps/session/service/UserSessionService.java b/src/main/java/com/eactive/apim/portal/apps/session/service/UserSessionService.java
index a69bf17..38c01c9 100644
--- a/src/main/java/com/eactive/apim/portal/apps/session/service/UserSessionService.java
+++ b/src/main/java/com/eactive/apim/portal/apps/session/service/UserSessionService.java
@@ -74,6 +74,18 @@ public class UserSessionService {
}
}
+ /**
+ * 해당 사용자의 모든 세션에 강제 로그아웃 플래그 설정.
+ * 관리자의 권한 변경(위임/회수/소속제외)을 대상 사용자의 활성 세션에 반영하기 위해 사용한다.
+ */
+ @Transactional
+ public void forceLogoutAllSessions(String loginId) {
+ int count = userSessionRepository.forceLogoutAllSessions(loginId);
+ if (count > 0) {
+ log.info("전체 강제 로그아웃 처리 - loginId: {}, 대상 세션 수: {}", loginId, count);
+ }
+ }
+
/**
* 현재 세션의 강제 로그아웃 여부 확인
*/
diff --git a/src/main/java/com/eactive/apim/portal/apps/user/controller/AccountController.java b/src/main/java/com/eactive/apim/portal/apps/user/controller/AccountController.java
index 8c884fd..4448fee 100644
--- a/src/main/java/com/eactive/apim/portal/apps/user/controller/AccountController.java
+++ b/src/main/java/com/eactive/apim/portal/apps/user/controller/AccountController.java
@@ -1,28 +1,18 @@
package com.eactive.apim.portal.apps.user.controller;
import com.eactive.apim.portal.apps.agreements.service.AgreementsFacade;
-import com.eactive.apim.portal.apps.user.dto.PasswordChangeRequestDTO;
-import com.eactive.apim.portal.apps.user.dto.PortalOrgRegistrationDTO;
-import com.eactive.apim.portal.apps.user.dto.PortalUserDTO;
-import com.eactive.apim.portal.apps.user.dto.UserAgreementDTO;
-import com.eactive.apim.portal.apps.user.dto.ValidationResponse;
+import com.eactive.apim.portal.apps.session.service.UserSessionService;
+import com.eactive.apim.portal.apps.user.dto.*;
import com.eactive.apim.portal.apps.user.facade.OrgRegisterFacade;
import com.eactive.apim.portal.apps.user.facade.UserFacade;
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
+import com.eactive.apim.portal.common.util.PhoneNumberUtil;
import com.eactive.apim.portal.common.util.SecurityUtil;
import com.eactive.apim.portal.invitation.entity.UserInvitation;
import com.eactive.apim.portal.invitation.entity.UserInvitationEnums.InvitationStatus;
import com.eactive.apim.portal.invitation.repository.UserInvitationRepository;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,14 +23,19 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.ModelAttribute;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
@Controller
@Secured("ROLE_ACCOUNT")
@RequiredArgsConstructor
@@ -53,6 +48,7 @@ public class AccountController {
private final AgreementsFacade agreementsFacade;
private final com.eactive.apim.portal.apps.user.facade.AuthFacade authFacade;
private final UserInvitationRepository userInvitationRepository;
+ private final UserSessionService userSessionService;
@PostMapping("/confirm_password")
@@ -105,6 +101,12 @@ public class AccountController {
session.removeAttribute("success");
session.removeAttribute("redirectUrl");
+ // 세션 무효화 전에 DB 세션 레코드를 정리한다.
+ // SecurityContextLogoutHandler 는 HTTP 세션만 invalidate 하고 UserSession DB 레코드는
+ // 남기므로(정상 로그아웃의 PortalLogoutSuccessHandler 경로를 우회), 정리하지 않으면
+ // 재로그인 시 check-duplicate 가 이 옛 세션을 활성으로 보고 "이미 접속중" 으로 오탐한다.
+ userSessionService.removeSession(session.getId());
+
new SecurityContextLogoutHandler().logout(request, response,
SecurityContextHolder.getContext().getAuthentication());
@@ -143,7 +145,7 @@ public class AccountController {
if (currentUser.getRoleCode() == RoleCode.ROLE_USER) {
java.util.Optional pendingInvitation =
userInvitationRepository.findFirstByInvitationMobileAndStatus(
- currentUser.getMobileNumber(), InvitationStatus.PENDING);
+ PhoneNumberUtil.normalize(currentUser.getMobileNumber()), InvitationStatus.PENDING);
if (pendingInvitation.isPresent()) {
mav.addObject("hasPendingInvitation", true);
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 c1da5b3..3c1974f 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
@@ -7,9 +7,11 @@ import com.eactive.apim.portal.apps.user.dto.ValidationResponse;
import com.eactive.apim.portal.apps.user.facade.AuthFacade;
import com.eactive.apim.portal.apps.user.facade.UserRegisterFacade;
import com.eactive.apim.portal.apps.user.repository.PortalOrgRepository;
+import com.eactive.apim.portal.apps.user.service.PortalUserAuthService;
import com.eactive.apim.portal.apps.user.service.PortalUserService;
import com.eactive.apim.portal.apps.user.validator.AgreementValidator;
import com.eactive.apim.portal.common.util.EncryptionUtil;
+import com.eactive.apim.portal.common.util.PhoneNumberUtil;
import com.eactive.apim.portal.common.util.SecurityUtil;
import com.eactive.apim.portal.config.PortalProperties;
import com.eactive.apim.portal.invitation.entity.UserInvitation;
@@ -63,6 +65,7 @@ public class UserRegisterController {
private final UserInvitationRepository userInvitationRepository;
private final EncryptionUtil encryptionUtil;
private final AgreementValidator agreementValidator;
+ private final PortalUserAuthService portalUserAuthService;
@GetMapping("/signup")
public String showSignupSelection() {
@@ -243,6 +246,11 @@ public class UserRegisterController {
String decodedToken = decodeInvitationToken(invitationToken);
session.setAttribute("decisionToken", decodedToken);
+ // 이미 로그인된 상태면 수락 화면으로 직행
+ if (SecurityUtil.isAuthenticated()) {
+ return "redirect:/signup/decision_process";
+ }
+
Optional invitation = userInvitationRepository.findByToken(decodedToken);
if (!invitation.isPresent() || invitation.get().getStatus() != UserInvitationEnums.InvitationStatus.PENDING) {
@@ -281,7 +289,7 @@ public class UserRegisterController {
public String showDecisionProcessPage(HttpSession session, Model model) {
session.removeAttribute("decisionToken");
- String mobile = SecurityUtil.getPortalAuthenticatedUser().getMobileNumber();
+ String mobile = PhoneNumberUtil.normalize(SecurityUtil.getPortalAuthenticatedUser().getMobileNumber());
Optional invitation = userInvitationRepository.findFirstByInvitationMobileAndStatus(mobile, UserInvitationEnums.InvitationStatus.PENDING);
if (!invitation.isPresent()) {
@@ -305,6 +313,8 @@ public class UserRegisterController {
model.addAttribute("invitationCode", invitation.get().getToken());
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT"));
+ model.addAttribute("notificationConsent", agreementsFacade.getAgreement("NOTIFICATION_CONSENT"));
+ model.addAttribute("agreementTitle", "법인 회원 전환을 위한 약관 동의");
model.addAttribute("registrationType", "corporate");
return "apps/register/userDecisionProcess";
@@ -323,7 +333,7 @@ public class UserRegisterController {
agreementValidator.validate(agreement, bindingResult);
- Optional invitation = userInvitationRepository.findFirstByInvitationMobileAndStatus(SecurityUtil.getPortalAuthenticatedUser().getMobileNumber(), UserInvitationEnums.InvitationStatus.PENDING);
+ Optional invitation = userInvitationRepository.findFirstByInvitationMobileAndStatus(PhoneNumberUtil.normalize(SecurityUtil.getPortalAuthenticatedUser().getMobileNumber()), UserInvitationEnums.InvitationStatus.PENDING);
if (action.equalsIgnoreCase("accept") && bindingResult.hasErrors()) {
@@ -334,12 +344,19 @@ public class UserRegisterController {
model.addAttribute("orgName", org.get().getOrgName());
model.addAttribute("termsOfUse", agreementsFacade.getAgreement("TERMS_OF_USE"));
model.addAttribute("privacyCollect", agreementsFacade.getAgreement("PRIVACY_COLLECT"));
+ model.addAttribute("notificationConsent", agreementsFacade.getAgreement("NOTIFICATION_CONSENT"));
+ model.addAttribute("agreementTitle", "법인 회원 전환을 위한 약관 동의");
model.addAttribute("registrationType", "corporate");
return "apps/register/userDecisionProcess";
} else {
ValidationResponse response = userRegisterFacade.processInvitation(action, invitation.get());
+ // 수락 성공 시 재로그인 없이 본인 세션 권한 갱신 (ROLE_USER → ROLE_CORP_USER)
+ if ("accept".equalsIgnoreCase(action) && response.isValid()) {
+ portalUserAuthService.reloadCurrentAuthentication();
+ }
+
// 초대 처리 완료 후 세션에서 초대 관련 속성 제거
session.removeAttribute("pendingInvitation");
session.removeAttribute("pendingInvitationToken");
diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserManFacade.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserManFacade.java
index e038bb4..ae663dc 100644
--- a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserManFacade.java
+++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserManFacade.java
@@ -7,6 +7,7 @@ import com.eactive.apim.portal.apps.user.mapper.PortalUserMapper;
import com.eactive.apim.portal.common.exception.NotFoundException;
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
import com.eactive.apim.portal.common.util.DurationParser;
+import com.eactive.apim.portal.common.util.PhoneNumberUtil;
import com.eactive.apim.portal.common.util.SecurityUtil;
import com.eactive.apim.portal.common.util.StringMaskingUtil;
import com.eactive.apim.portal.common.validator.CellPhoneValidator;
@@ -19,6 +20,8 @@ import com.eactive.apim.portal.invitation.repository.UserInvitationRepository;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.UserStatus;
+import com.eactive.apim.portal.apps.session.service.UserSessionService;
+import com.eactive.apim.portal.apps.user.service.PortalUserAuthService;
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.eactive.apim.portal.template.service.MessageHandlerService;
@@ -54,6 +57,8 @@ public class UserManFacade {
private final UserLogRepository userLogRepository;
private final PortalPropertyService portalPropertyService;
private final CellPhoneValidator cellPhoneValidator;
+ private final UserSessionService userSessionService;
+ private final PortalUserAuthService portalUserAuthService;
public Page getUsers(PortalOrgDTO userOrg, Pageable pageable) {
return portalUserRepository
@@ -178,16 +183,7 @@ public class UserManFacade {
* 010-XXXX-XXXX(11자리) / 010-XXX-XXXX(10자리). 형식 불명은 원본 반환(검증에서 차단됨).
*/
private String normalizeMobile(String raw) {
- if (raw == null) {
- return null;
- }
- String digits = raw.replaceAll("[^0-9]", "");
- if (digits.length() == 11) {
- return digits.substring(0, 3) + "-" + digits.substring(3, 7) + "-" + digits.substring(7);
- } else if (digits.length() == 10) {
- return digits.substring(0, 3) + "-" + digits.substring(3, 6) + "-" + digits.substring(6);
- }
- return raw;
+ return PhoneNumberUtil.normalize(raw);
}
public String createInvitation(PortalUser sender, String mobile) {
@@ -253,6 +249,10 @@ public class UserManFacade {
portalUserRepository.save(targetUser);
portalUserRepository.save(currentUser);
+ // 권한 변경 즉시 반영: 대상(타 세션)은 강제 로그아웃, 본인(현재 세션)은 in-place 재인증
+ userSessionService.forceLogoutAllSessions(targetUser.getLoginId());
+ portalUserAuthService.reloadCurrentAuthentication();
+
MessageRecipient recipient = new MessageRecipient();
recipient.setUserId(targetUser.getEmailAddr());
recipient.setPhone(targetUser.getMobileNumber());
@@ -284,6 +284,9 @@ public class UserManFacade {
targetUser.setRoleCode(RoleCode.ROLE_CORP_USER);
portalUserRepository.save(targetUser);
+
+ // 권한 회수를 대상 사용자의 활성 세션에 반영 (강제 로그아웃 → 재로그인 시 새 권한)
+ userSessionService.forceLogoutAllSessions(targetUser.getLoginId());
}
/**
@@ -305,6 +308,9 @@ public class UserManFacade {
targetUser.setPortalOrg(null);
targetUser.setRoleCode(RoleCode.ROLE_USER);
portalUserRepository.save(targetUser);
+
+ // 소속 제외를 대상 사용자의 활성 세션에 반영 (강제 로그아웃 → 재로그인 시 새 권한)
+ userSessionService.forceLogoutAllSessions(targetUser.getLoginId());
}
public void cancelInvitation(PortalAuthenticatedUser user, String invitationId, boolean notifyConsent) {
diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserRegisterFacadeImpl.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserRegisterFacadeImpl.java
index 436918a..f1800e5 100644
--- a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserRegisterFacadeImpl.java
+++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserRegisterFacadeImpl.java
@@ -263,7 +263,7 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade {
ValidationResponse response = new ValidationResponse();
response.setValid(true);
- response.setMessage("초대가 성공적으로 수락되었습니다. 로그아웃 후 다시 로그인 해 주세요.");
+ response.setMessage("초대가 성공적으로 수락되었습니다. 법인회원으로 전환되었습니다.");
response.setShowPopup(true);
response.setPopupType("success");
return response;
diff --git a/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserAuthService.java b/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserAuthService.java
index de3b532..2b113fb 100644
--- a/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserAuthService.java
+++ b/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserAuthService.java
@@ -6,6 +6,7 @@ import com.eactive.apim.portal.common.exception.SystemException;
import com.eactive.apim.portal.common.exception.UserNotFoundException;
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
import com.eactive.apim.portal.common.util.EncryptionUtil;
+import com.eactive.apim.portal.common.util.SecurityUtil;
import com.eactive.apim.portal.common.util.StringMaskingUtil;
import com.eactive.apim.portal.config.PortalProperties;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
@@ -30,7 +31,9 @@ import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import lombok.RequiredArgsConstructor;
import org.apache.xerces.impl.dv.util.Base64;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -58,21 +61,46 @@ public class PortalUserAuthService implements UserDetailsService {
// 이메일 소문자 변환 적용 (대소문자 구분 없이 로그인 가능하도록)
String normalizedUsername = username != null ? username.toLowerCase() : null;
PortalUser portalUser = findByEmailAddr(normalizedUsername);
- RoleCode userRole = portalUser.getRoleCode() == null ? RoleCode.ROLE_USER : portalUser.getRoleCode();
- List roles = portalProperties.getPortalSecurity().get(userRole);
- PortalAuthenticatedUser authenticatedUser = portalUserMapper.portalUserToAuthenticatedUser(portalUser);
-
- authenticatedUser.getAuthorities().add(new SimpleGrantedAuthority(userRole.name()));
-
- roles.forEach(role -> authenticatedUser.getAuthorities().add(new SimpleGrantedAuthority(role)));
-
- return authenticatedUser;
-
+ return buildAuthenticatedUser(portalUser);
} catch (UserNotFoundException e) {
throw new UsernameNotFoundException("입력하신 사용자 정보가 올바르지 않습니다. 다시 확인해 주세요.");
}
}
+ /**
+ * DB의 PortalUser 로부터 권한(roleCode + 역할 계층)을 채운 PortalAuthenticatedUser 를 생성한다.
+ * 로그인과 세션 재로드가 동일한 권한 구성을 사용하도록 공통화한다.
+ */
+ public PortalAuthenticatedUser buildAuthenticatedUser(PortalUser portalUser) {
+ RoleCode userRole = portalUser.getRoleCode() == null ? RoleCode.ROLE_USER : portalUser.getRoleCode();
+ List roles = portalProperties.getPortalSecurity().get(userRole);
+ PortalAuthenticatedUser authenticatedUser = portalUserMapper.portalUserToAuthenticatedUser(portalUser);
+
+ authenticatedUser.getAuthorities().add(new SimpleGrantedAuthority(userRole.name()));
+ roles.forEach(role -> authenticatedUser.getAuthorities().add(new SimpleGrantedAuthority(role)));
+
+ return authenticatedUser;
+ }
+
+ /**
+ * 현재 로그인한 사용자의 권한을 DB에서 다시 읽어 SecurityContext 의 Authentication 을 교체한다.
+ * 재로그인 없이 본인 세션에 역할 변경(초대 수락, 관리자 위임으로 인한 본인 강등 등)을 즉시 반영한다.
+ */
+ public void reloadCurrentAuthentication() {
+ PortalAuthenticatedUser current = SecurityUtil.getPortalAuthenticatedUser();
+ if (current == null) {
+ return;
+ }
+ PortalUser fresh = portalUserRepository.findByLoginId(current.getLoginId())
+ .orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다."));
+ PortalAuthenticatedUser refreshed = buildAuthenticatedUser(fresh);
+
+ UsernamePasswordAuthenticationToken newAuth =
+ new UsernamePasswordAuthenticationToken(refreshed, null, refreshed.getAuthorities());
+ newAuth.setDetails(refreshed);
+ SecurityContextHolder.getContext().setAuthentication(newAuth);
+ }
+
public List findAllUsersByNameAndMobile(String userName, String mobileNumber) {
try {
if (mobileNumber == null || !mobileNumber.matches("^\\d{2,3}-\\d{3,4}-\\d{4}$")) {
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 6e968d4..f771276 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
@@ -2,6 +2,7 @@ package com.eactive.apim.portal.apps.user.service;
import com.eactive.apim.portal.apps.user.dto.PortalUserRegistrationDTO;
import com.eactive.apim.portal.apps.user.repository.PortalOrgRepository;
+import com.eactive.apim.portal.common.util.PhoneNumberUtil;
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
@@ -83,7 +84,7 @@ public class PortalUserService {
if (mobileNumber == null || mobileNumber.trim().isEmpty()) {
return false;
}
- return portalUserRepository.existsByMobileNumber(mobileNumber.trim());
+ return portalUserRepository.existsByMobileNumber(PhoneNumberUtil.normalize(mobileNumber));
}
/**
diff --git a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationSuccessHandler.java b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationSuccessHandler.java
index 1349278..afd52ae 100644
--- a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationSuccessHandler.java
+++ b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationSuccessHandler.java
@@ -4,6 +4,7 @@ import com.eactive.apim.portal.apps.session.service.UserSessionService;
import com.eactive.apim.portal.apps.user.repository.PortalOrgRepository;
import com.eactive.apim.portal.apps.user.service.PortalUserLogService;
import com.eactive.apim.portal.common.util.HttpRequestUtil;
+import com.eactive.apim.portal.common.util.PhoneNumberUtil;
import com.eactive.apim.portal.common.util.StringRepeatUtil;
import com.eactive.apim.portal.invitation.entity.UserInvitation;
import com.eactive.apim.portal.invitation.entity.UserInvitationEnums.InvitationStatus;
@@ -95,9 +96,10 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess
// 초대 코드 확인 - ROLE_USER만 확인 (세션에 저장하여 메인 페이지에서 팝업으로 표시)
if (user.getRoleCode() == PortalUserEnums.RoleCode.ROLE_USER) {
+ // 휴대폰 형식(하이픈 유무)이 달라도 초대와 매칭되도록 정규화 후 조회
Optional pendingInvitation =
userInvitationRepository.findFirstByInvitationMobileAndStatus(
- user.getMobileNumber(), InvitationStatus.PENDING);
+ PhoneNumberUtil.normalize(user.getMobileNumber()), InvitationStatus.PENDING);
if (pendingInvitation.isPresent()) {
UserInvitation invitation = pendingInvitation.get();
diff --git a/src/main/resources/templates/views/apps/register/userAgreementContent.html b/src/main/resources/templates/views/apps/register/userAgreementContent.html
index c0922bb..15b174a 100644
--- a/src/main/resources/templates/views/apps/register/userAgreementContent.html
+++ b/src/main/resources/templates/views/apps/register/userAgreementContent.html
@@ -7,7 +7,7 @@
diff --git a/src/main/resources/templates/views/apps/register/userDecisionAccept.html b/src/main/resources/templates/views/apps/register/userDecisionAccept.html
index e55a118..ac0d3d6 100644
--- a/src/main/resources/templates/views/apps/register/userDecisionAccept.html
+++ b/src/main/resources/templates/views/apps/register/userDecisionAccept.html
@@ -32,16 +32,6 @@
-
-
- 초대코드:
-
-
-
- 이메일로 받은 초대코드와 일치하는지 확인해 주세요.
-
-
-
diff --git a/src/main/resources/templates/views/apps/register/userDecisionProcess.html b/src/main/resources/templates/views/apps/register/userDecisionProcess.html
index 1d1288e..c62de6d 100644
--- a/src/main/resources/templates/views/apps/register/userDecisionProcess.html
+++ b/src/main/resources/templates/views/apps/register/userDecisionProcess.html
@@ -43,15 +43,6 @@
-
-
- 초대코드:
-
-
-
- 이메일로 받은 초대코드와 일치하는지 확인해 주세요.
-
-
diff --git a/src/test/java/com/eactive/apim/portal/common/util/PhoneNumberUtilTest.java b/src/test/java/com/eactive/apim/portal/common/util/PhoneNumberUtilTest.java
new file mode 100644
index 0000000..51052e8
--- /dev/null
+++ b/src/test/java/com/eactive/apim/portal/common/util/PhoneNumberUtilTest.java
@@ -0,0 +1,70 @@
+package com.eactive.apim.portal.common.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class PhoneNumberUtilTest {
+
+ @Test
+ @DisplayName("휴대폰 11자리 → 010-XXXX-XXXX (멱등)")
+ void mobile11() {
+ assertEquals("010-1234-5678", PhoneNumberUtil.normalize("01012345678"));
+ assertEquals("010-1234-5678", PhoneNumberUtil.normalize("010-1234-5678"));
+ assertEquals("010-9912-0000", PhoneNumberUtil.normalize("01099120000"));
+ }
+
+ @Test
+ @DisplayName("휴대폰 10자리 → 01X-XXX-XXXX")
+ void mobile10() {
+ assertEquals("011-123-4567", PhoneNumberUtil.normalize("0111234567"));
+ }
+
+ @Test
+ @DisplayName("서울 02 → 9자리 02-XXX-XXXX / 10자리 02-XXXX-XXXX")
+ void seoul() {
+ assertEquals("02-123-4567", PhoneNumberUtil.normalize("021234567"));
+ assertEquals("02-1234-5678", PhoneNumberUtil.normalize("0212345678"));
+ assertEquals("02-1234-5678", PhoneNumberUtil.normalize("02-1234-5678"));
+ }
+
+ @Test
+ @DisplayName("지역/070 → 10자리 0XX-XXX-XXXX / 11자리 0XX-XXXX-XXXX")
+ void area() {
+ assertEquals("031-234-5678", PhoneNumberUtil.normalize("0312345678"));
+ assertEquals("070-1234-5678", PhoneNumberUtil.normalize("07012345678"));
+ }
+
+ @Test
+ @DisplayName("안심번호 050X 12자리 → 0XXX-XXXX-XXXX")
+ void safetyNumber() {
+ assertEquals("0507-1234-5678", PhoneNumberUtil.normalize("050712345678"));
+ }
+
+ @Test
+ @DisplayName("대표번호 15XX/16XX/18XX 8자리 → XXXX-XXXX")
+ void representative() {
+ assertEquals("1588-1234", PhoneNumberUtil.normalize("15881234"));
+ assertEquals("1644-5678", PhoneNumberUtil.normalize("16445678"));
+ assertEquals("1800-1234", PhoneNumberUtil.normalize("18001234"));
+ }
+
+ @Test
+ @DisplayName("미해당 패턴/null/blank 는 원본 유지")
+ void passthrough() {
+ assertEquals("123", PhoneNumberUtil.normalize("123"));
+ assertEquals("abc", PhoneNumberUtil.normalize("abc"));
+ assertEquals("", PhoneNumberUtil.normalize(""));
+ assertNull(PhoneNumberUtil.normalize(null));
+ }
+
+ @Test
+ @DisplayName("digitsOnly 는 숫자만 추출, null 유지")
+ void digitsOnly() {
+ assertEquals("01012345678", PhoneNumberUtil.digitsOnly("010-1234-5678"));
+ assertEquals("0212345678", PhoneNumberUtil.digitsOnly("02-1234-5678"));
+ assertNull(PhoneNumberUtil.digitsOnly(null));
+ }
+}