From 829630ae5c448d0dfca79c5ffde24d251d0af8c6 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Thu, 30 Jul 2026 11:24:05 +0900 Subject: [PATCH 1/4] =?UTF-8?q?API=20=EC=8A=A4=ED=8E=99=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EB=AC=B8=EA=B5=AC=20=EB=B0=8F=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0=20-=20=EA=B2=8C=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=EC=9B=A8=EC=9D=B4=20=EA=B4=80=EB=A0=A8=20=EB=AC=B8=EA=B5=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EA=B7=B8=EB=A3=B9=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=95=88=EB=82=B4=20=EC=B6=94=EA=B0=80=20-=20?= =?UTF-8?q?=EB=B9=84=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EA=B3=B5=EA=B0=9C=20=EA=B4=80=EB=A0=A8=20=EB=AC=B8?= =?UTF-8?q?=EA=B5=AC=20=EC=88=98=EC=A0=95=20-=20description=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=A0=80=EC=9E=A5/=EB=B3=B5=EC=9B=90=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AccountController.java | 7 ++ .../user/facade/OrgRegisterFacadeImpl.java | 20 +++++- .../portal/apps/user/facade/UserFacade.java | 3 + .../apps/user/facade/UserFacadeImpl.java | 6 ++ .../user/facade/UserRegisterFacadeImpl.java | 3 +- .../apps/user/service/PasswordService.java | 14 ++++ .../user/service/PortalUserAuthService.java | 5 +- .../common/security/LoginLockPolicy.java | 44 ++++++++++++ .../validator/PasswordRuleValidator.java | 48 ++++++++----- .../PortalAuthenticationFailureHandler.java | 11 ++- .../config/PortalAuthenticationManager.java | 4 +- .../custom/config/DjbPasswordEncoder.java | 4 -- .../resources/static/js/password-policy.js | 70 ++++++++++++++++--- .../static/js/popup/custom-popups.js | 6 +- .../views/apps/mypage/passwordChange.html | 51 +++++++++++++- .../apps/mypage/updateCorporateManager.html | 7 +- .../apps/mypage/updateCorporateUser.html | 5 +- .../register/components/newUserInfoForm.html | 12 +++- .../views/fragment/popup/userInvitePopup.html | 2 +- 19 files changed, 274 insertions(+), 48 deletions(-) create mode 100644 src/main/java/com/eactive/apim/portal/common/security/LoginLockPolicy.java 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 1a8b8bd..2f90a16 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 @@ -57,6 +57,13 @@ public class AccountController { private final TwoFactorProperties twoFactorProperties; + /** 비밀번호 변경 화면 라이브 체크: 입력 중인 비밀번호에 아이디/휴대전화가 포함되는지 (민감정보는 응답에 미포함) */ + @PostMapping("/password/content-check") + public ResponseEntity> checkPasswordContent(@RequestParam String password) { + String currentLoginId = SecurityUtil.getCurrentLoginId(); + return ResponseEntity.ok(userFacade.checkPasswordContent(currentLoginId, password)); + } + @PostMapping("/password/confirm") public ResponseEntity confirmPassword(@RequestParam String inputPassword) { String currentLoginId = SecurityUtil.getCurrentLoginId(); 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 6c5f77c..2786224 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 @@ -54,6 +54,7 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade { private final FileService fileService; private final BasicValidationService validationService; private final UserRegistrationValidationService userRegistrationValidationService; + private final com.eactive.apim.portal.apps.user.validator.PasswordValidator passwordValidator; private final PasswordEncoder passwordEncoder; private final AgreementValidator agreementValidator; private final ApprovalService approvalService; @@ -75,14 +76,29 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade { return new ValidationResponse(false, "입력 정보가 올바르지 않습니다."); } - // 2025.10.20 - 휴대폰 번호 중복 무시 -// PortalUser existingUser = portalUserRepository.findByUserNameAndMobileNumber(orgDTO.getUserName(), orgDTO.getMobileNumber()); + // 개인 가입(@Valid @PasswordRule)과 달리 법인 가입은 컨트롤러 바인딩 검증이 없어 + // 여기서 서버 측 비밀번호 규칙을 직접 검증한다 (retain/change 시나리오는 기존 비밀번호 유지라 제외) + if (!passwordValidator.isValidPassword(orgDTO.getPassword(), orgDTO.getLoginId(), orgDTO.getMobileNumber())) { + return new ValidationResponse(false, + "비밀번호는 영문/숫자/특수문자 포함 8~50자이며, 아이디·휴대전화 번호, 3자리 이상 연속·반복 문자는 사용할 수 없습니다."); + } + + if (orgDTO.getConfirmPassword() == null || !orgDTO.getConfirmPassword().equals(orgDTO.getPassword())) { + return new ValidationResponse(false, "비밀번호와 비밀번호 확인이 일치하지 않습니다."); + } + Optional existingUser = portalUserRepository.findByLoginId(orgDTO.getLoginId()); if(existingUser.isPresent()) { return new ValidationResponse(false, "가입된 계정이 이미 존재합니다."); } + // 휴대폰 번호 중복 검증 (Portal/user.mobile.duplicate.allow 프로퍼티에 따라 차단) + if (portalUserService.isMobileDuplicateCheckEnabled() + && portalUserService.existsByMobileNumber(orgDTO.getMobileNumber())) { + return new ValidationResponse(false, "이미 가입된 휴대폰 번호입니다."); + } + try { FileInfo uploadedFile = handleFileUpload(orgDTO.getFiles()); if (uploadedFile == null) { diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacade.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacade.java index 98cd3c2..c6a2a18 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacade.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacade.java @@ -10,6 +10,9 @@ public interface UserFacade { void updatePassword(String loginId, String newPassword, String confirmPassword); + /** 비밀번호에 아이디/휴대전화 번호가 포함되는지 라이브 체크용 판정 (키: idIncluded, mobileIncluded) */ + java.util.Map checkPasswordContent(String loginId, String password); + void updateUser(PortalUserDTO portalUserDTO); void updateCorporateManager(PortalUserDTO portalUserDTO); diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacadeImpl.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacadeImpl.java index 40ab6a7..db69e52 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacadeImpl.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacadeImpl.java @@ -60,6 +60,12 @@ public class UserFacadeImpl implements UserFacade { messageHandlerService.publishEvent(UserPasswordChangedEvent.KEY, recipient, params); } + @Override + @Transactional(readOnly = true) + public HashMap checkPasswordContent(String loginId, String password) { + return passwordService.checkPasswordContent(loginId, password); + } + @Override @Transactional public void updateUser(PortalUserDTO portalUserDTO) { 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 58b4fe3..3ef2f33 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 @@ -187,7 +187,8 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade { return new ValidationResponse(false, "이미 가입된 휴대폰 번호입니다."); } - PortalUser existingUser = portalUserRepository.findByUserNameAndMobileNumber(registrationDTO.getUserName(), registrationDTO.getMobileNumber()); + PortalUser existingUser = portalUserRepository.findByUserNameAndMobileNumber(registrationDTO.getUserName(), + com.eactive.apim.portal.common.util.PhoneNumberUtil.normalize(registrationDTO.getMobileNumber())); if(existingUser != null) { return new ValidationResponse(false, "가입된 계정이 이미 존재합니다."); diff --git a/src/main/java/com/eactive/apim/portal/apps/user/service/PasswordService.java b/src/main/java/com/eactive/apim/portal/apps/user/service/PasswordService.java index 4b74da7..cd39377 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/service/PasswordService.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/service/PasswordService.java @@ -1,6 +1,7 @@ package com.eactive.apim.portal.apps.user.service; import com.eactive.apim.portal.common.dto.PasswordValidationDTO; +import com.eactive.apim.portal.common.validator.PasswordRuleValidator; import com.eactive.apim.portal.portaluser.entity.PortalUser; import com.eactive.apim.portal.portaluser.entity.UserPasswordHistory; import com.eactive.apim.portal.portaluser.repository.PortalUserRepository; @@ -71,6 +72,19 @@ public class PasswordService { } } + /** 비밀번호에 본인 아이디(local part)/휴대전화 세그먼트가 포함되는지 판정 — 변경 화면 라이브 체크용 */ + @Transactional(readOnly = true) + public java.util.HashMap checkPasswordContent(String loginId, String password) { + PortalUser user = portalUserRepository.findByLoginId(loginId) + .orElseThrow(() -> new IllegalArgumentException("해당 사용자를 찾을 수 없습니다.")); + java.util.HashMap result = new java.util.HashMap<>(); + result.put("idIncluded", + PasswordRuleValidator.containsLoginIdLocalPart(password, user.getLoginId())); + result.put("mobileIncluded", + PasswordRuleValidator.containsMobileSegment(password, user.getMobileNumber())); + return result; + } + private void checkPasswordHistory(String userId, String newPassword) { List passwordHistories = passwordHistoryRepository.findRecentPasswordsByUserId(userId); 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 fe791bd..b6b18ab 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 @@ -5,6 +5,7 @@ import com.eactive.apim.portal.apps.login.service.LoginFinalizer; import com.eactive.apim.portal.apps.user.dto.PortalUserDTO; import com.eactive.apim.portal.apps.user.mapper.PortalUserMapper; import com.eactive.apim.portal.common.exception.SystemException; +import com.eactive.apim.portal.common.util.PhoneNumberUtil; import com.eactive.apim.portal.common.exception.UserNotFoundException; import com.eactive.apim.portal.common.user.PortalAuthenticatedUser; import com.eactive.apim.portal.common.util.EncryptionUtil; @@ -151,6 +152,8 @@ public class PortalUserAuthService implements UserDetailsService { } public void resetPassword(String loginId, String userName, String mobileNumber) { + // 입력 그룹핑이 저장 정규형과 달라도 매칭되도록 조회 전 정규화 (암호화 컬럼 등가 비교) + mobileNumber = PhoneNumberUtil.normalize(mobileNumber); if (mobileNumber == null || !mobileNumber.matches("^\\d{2,3}-\\d{3,4}-\\d{4}$")) { throw new UserNotFoundException("유효하지 않은 휴대폰 번호 형식입니다."); } @@ -185,7 +188,7 @@ public class PortalUserAuthService implements UserDetailsService { @Transactional public void reactivateDormantAccount(String loginId, String password, String mobileNumber) { try { - PortalUser portalUser = portalUserRepository.findByLoginIdAndMobileNumber(loginId, mobileNumber) + PortalUser portalUser = portalUserRepository.findByLoginIdAndMobileNumber(loginId, PhoneNumberUtil.normalize(mobileNumber)) .orElseThrow(() -> new UserNotFoundException("입력하신 사용자 정보가 올바르지 않습니다. 다시 확인해 주세요.")); if (!passwordEncoder.matches(password, portalUser.getPasswordHash())) { diff --git a/src/main/java/com/eactive/apim/portal/common/security/LoginLockPolicy.java b/src/main/java/com/eactive/apim/portal/common/security/LoginLockPolicy.java new file mode 100644 index 0000000..5ccc4f0 --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/common/security/LoginLockPolicy.java @@ -0,0 +1,44 @@ +package com.eactive.apim.portal.common.security; + +import com.eactive.apim.portal.portalproperty.service.PortalPropertyService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 로그인 실패 계정 잠금 임계 횟수를 DB(PortalProperty)에서 조회한다. + * + *

PTL_PROPERTY (group={@code Portal}, name={@code login.failure.lock.count}) 값으로 제어한다. + * 값이 없으면 기본값 {@value #DEFAULT_LOCK_COUNT}로 자동 생성되고, 숫자가 아니거나 + * 0 이하이면 기본값으로 동작한다.

+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class LoginLockPolicy { + + private static final String GROUP = "Portal"; + private static final String NAME = "login.failure.lock.count"; + + /** 기본 잠금 임계 횟수 (프로퍼티 미존재/파싱 실패 시) */ + public static final int DEFAULT_LOCK_COUNT = 5; + + private final PortalPropertyService portalPropertyService; + + /** 연속 로그인 실패가 이 값 이상이면 계정을 잠근다. */ + public int lockCount() { + String raw = portalPropertyService.getOrCreateProperty( + GROUP, NAME, String.valueOf(DEFAULT_LOCK_COUNT), + "로그인 연속 실패 계정 잠금 임계 횟수 (이 값 이상 실패 시 잠금)"); + try { + int parsed = Integer.parseInt(raw.trim()); + if (parsed > 0) { + return parsed; + } + log.warn("login.failure.lock.count 값이 0 이하({}) - 기본값 {} 사용", parsed, DEFAULT_LOCK_COUNT); + } catch (NumberFormatException e) { + log.warn("login.failure.lock.count 값이 숫자가 아님('{}') - 기본값 {} 사용", raw, DEFAULT_LOCK_COUNT); + } + return DEFAULT_LOCK_COUNT; + } +} diff --git a/src/main/java/com/eactive/apim/portal/common/validator/PasswordRuleValidator.java b/src/main/java/com/eactive/apim/portal/common/validator/PasswordRuleValidator.java index 178d00f..717df43 100644 --- a/src/main/java/com/eactive/apim/portal/common/validator/PasswordRuleValidator.java +++ b/src/main/java/com/eactive/apim/portal/common/validator/PasswordRuleValidator.java @@ -1,5 +1,6 @@ package com.eactive.apim.portal.common.validator; +import com.eactive.apim.portal.common.util.PhoneNumberUtil; import org.apache.commons.beanutils.PropertyUtils; import javax.validation.ConstraintValidator; @@ -72,24 +73,12 @@ public class PasswordRuleValidator implements ConstraintValidator 0) { - String username = loginParts[0].toUpperCase(); - if (tmpPw.contains(username)) { - return false; - } - } + if (containsLoginIdLocalPart(password, loginId)) { + return false; } - // Mobile number validation - if (mobileNumber != null && !mobileNumber.isEmpty()) { - String[] mobileParts = mobileNumber.split("-"); - for (String part : mobileParts) { - if (!part.isEmpty() && tmpPw.contains(part)) { - return false; - } - } + if (containsMobileSegment(password, mobileNumber)) { + return false; } // 공백 체크 @@ -129,6 +118,33 @@ public class PasswordRuleValidator implements ConstraintValidator 47 && third < 58) || (first > 64 && third < 91); diff --git a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationFailureHandler.java b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationFailureHandler.java index 8e19da8..7da961f 100644 --- a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationFailureHandler.java +++ b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationFailureHandler.java @@ -4,6 +4,7 @@ import com.eactive.apim.portal.apps.login.constants.LoginConstants; import com.eactive.apim.portal.apps.login.constants.LoginFailureReason; import com.eactive.apim.portal.apps.user.service.PortalUserLogService; import com.eactive.apim.portal.common.exception.UserNotFoundException; +import com.eactive.apim.portal.common.security.LoginLockPolicy; import com.eactive.apim.portal.common.util.HttpRequestUtil; import com.eactive.apim.portal.common.util.StringMaskingUtil; import com.eactive.apim.portal.common.util.StringRepeatUtil; @@ -47,14 +48,17 @@ public class PortalAuthenticationFailureHandler implements AuthenticationFailure private final PortalUserRepository portalUserRepository; private final PortalUserLogService userLogService; private final MessageHandlerService messageHandlerService; + private final LoginLockPolicy loginLockPolicy; public PortalAuthenticationFailureHandler(PortalUserRepository portalUserRepository, PortalUserLogService userLogService, - MessageHandlerService messageHandlerService) { + MessageHandlerService messageHandlerService, + LoginLockPolicy loginLockPolicy) { this.portalUserRepository = portalUserRepository; this.userLogService = userLogService; this.messageHandlerService = messageHandlerService; + this.loginLockPolicy = loginLockPolicy; } @Override @@ -73,15 +77,16 @@ public class PortalAuthenticationFailureHandler implements AuthenticationFailure PortalUser user = portalUserRepository.findPortalUserByEmailAddr(normalizedUsername) .orElseThrow(() -> new UserNotFoundException(normalizedUsername)); + int lockCount = loginLockPolicy.lockCount(); user.setLoginFailureCount(user.getLoginFailureCount() + 1); - if (user.getLoginFailureCount() >= 5) { + if (user.getLoginFailureCount() >= lockCount) { user.setAccountLockYn("Y"); // 계정 잠금 알림 messageHandlerService.publishEvent( MessageCode.USER_ACCOUNT_LOCKED, MessageRecipient.of(user), - Maps.of("reason", "5회 이상 로그인 실패로 인한 계정 잠금")) ;; + Maps.of("reason", lockCount + "회 이상 로그인 실패로 인한 계정 잠금")); } portalUserRepository.save(user); 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 f4eb214..f08555d 100644 --- a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationManager.java +++ b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationManager.java @@ -2,6 +2,7 @@ package com.eactive.apim.portal.config; import com.eactive.apim.portal.apps.user.service.PortalUserAuthService; +import com.eactive.apim.portal.common.security.LoginLockPolicy; import com.eactive.apim.portal.common.user.PortalAuthenticatedUser; import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums; import com.eactive.apim.portal.portaluser.entity.PortalUserEnums; @@ -28,6 +29,7 @@ public class PortalAuthenticationManager implements AuthenticationManager { private final PortalUserAuthService portalUserAuthService; private final PasswordEncoder passwordEncoder; private final MessageHandlerService messageHandlerService; + private final LoginLockPolicy loginLockPolicy; @Override @Transactional(noRollbackFor = {AuthenticationException.class}) @@ -40,7 +42,7 @@ public class PortalAuthenticationManager implements AuthenticationManager { if (!user.isAccountNonLocked()) { - if (user.getLoginFailureCount() >= 5) { + if (user.getLoginFailureCount() >= loginLockPolicy.lockCount()) { throw new LockedException("계정이 잠겼습니다. 비밀번호 초기화 또는 관리자에게 문의하세요."); } diff --git a/src/main/java/com/eactive/apim/portal/custom/config/DjbPasswordEncoder.java b/src/main/java/com/eactive/apim/portal/custom/config/DjbPasswordEncoder.java index 0b966ac..239995f 100644 --- a/src/main/java/com/eactive/apim/portal/custom/config/DjbPasswordEncoder.java +++ b/src/main/java/com/eactive/apim/portal/custom/config/DjbPasswordEncoder.java @@ -1,6 +1,5 @@ package com.eactive.apim.portal.custom.config; -//import com.eactive.ext.djb.safedb.DjbSafedbWrapper; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @@ -17,8 +16,5 @@ public class DjbPasswordEncoder implements PasswordEncoder { @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return bcryptEncoder.matches(rawPassword, encodedPassword); -// DjbSafedbWrapper safedb = DjbSafedbWrapper.getInstance(); -// String bcryptHash = safedb.decryptNotRnno(encodedPassword); -// return bcryptEncoder.matches(rawPassword, bcryptHash); } } diff --git a/src/main/resources/static/js/password-policy.js b/src/main/resources/static/js/password-policy.js index 1bc8c77..d9dab8e 100644 --- a/src/main/resources/static/js/password-policy.js +++ b/src/main/resources/static/js/password-policy.js @@ -2,8 +2,10 @@ * 비밀번호 문자열 정책 라이브 검증 (공용) * * 서버 검증기 PasswordRuleValidator.isValid(= @PasswordRule) 의 - * "문자열" 규칙을 그대로 클라이언트로 포팅한다. 아이디/휴대전화 포함 여부는 - * 민감정보 노출을 피하기 위해 서버 검증에만 맡긴다. + * 규칙을 클라이언트로 포팅한다. 아이디/휴대전화 포함 규칙(noid/nomobile)은 + * 사용자가 폼에 직접 입력한 값이 페이지에 이미 있을 때만 ul 의 + * data-context-loginid/data-context-mobile 로 연결해 쓴다 — DB 값을 새로 + * 내려받아야 하는 화면(비밀번호 변경)은 서버 AJAX(/password/content-check)로 판정한다. * * 사용법(마크업 구동): *
    @@ -31,7 +33,7 @@ return false; } - // 규칙별 판정 함수 (통과=true) + // 규칙별 판정 함수 (통과=true). ctx = { loginId, mobile } — 값이 없으면 해당 규칙은 통과 처리 var RULES = { length: function (pw) { return pw.length >= 8 && pw.length <= 50; }, letter: function (pw) { return /[a-zA-Z]/.test(pw); }, @@ -39,23 +41,46 @@ special: function (pw) { return /[^A-Za-z0-9_]/.test(pw); }, // 서버 정규식 \W 기준 (밑줄 제외) nospace: function (pw) { return !/\s/.test(pw); }, norepeat: function (pw) { return !/(\w)\1\1/.test(pw.toUpperCase()); }, - noseq: function (pw) { return !hasSequential(pw); } + noseq: function (pw) { return !hasSequential(pw); }, + // 아이디(이메일 local part) 포함 금지 — 서버 PasswordRuleValidator.containsLoginIdLocalPart 포팅 + noid: function (pw, ctx) { + var id = ctx && ctx.loginId ? String(ctx.loginId).split('@')[0].toUpperCase() : ''; + return !id || pw.toUpperCase().indexOf(id) === -1; + }, + // 휴대전화 하이픈 세그먼트 포함 금지 — 서버 containsMobileSegment 포팅 + nomobile: function (pw, ctx) { + var m = ctx && ctx.mobile ? String(ctx.mobile) : ''; + if (!m) { return true; } + var up = pw.toUpperCase(); + var parts = m.split('-'); + for (var i = 0; i < parts.length; i++) { + if (parts[i] && up.indexOf(parts[i]) !== -1) { + return false; + } + } + return true; + } }; - // 전체 문자열 규칙 통과 여부 - function isValid(pw) { + // 전체 문자열 규칙 통과 여부 (ctx 미전달 시 noid/nomobile 은 통과 — 서버 검증에 위임) + function isValid(pw, ctx) { if (!pw) { return false; } for (var key in RULES) { - if (RULES.hasOwnProperty(key) && !RULES[key](pw)) { + if (RULES.hasOwnProperty(key) && !RULES[key](pw, ctx || {})) { return false; } } return true; } - // 체크리스트(ul) 하나를 대상 input 에 바인딩 + // bind 된 체크리스트들의 update 함수 목록 (컨텍스트 값 변경 시 refresh 용) + var updaters = []; + + // 체크리스트(ul) 하나를 대상 input 에 바인딩. + // ul 의 data-context-loginid / data-context-mobile 속성에 소스 input 의 id 를 주면 + // noid/nomobile 규칙이 해당 값 기준으로 라이브 판정된다. function bind(input, list) { var $input = (input && input.jquery) ? input : $(input); var $list = (list && list.jquery) ? list : $(list); @@ -64,8 +89,18 @@ return; } + function ctxValue(attr) { + var id = $list.attr(attr); + var el = id ? document.getElementById(id) : null; + return el ? el.value : ''; + } + function update() { var pw = $input.val() || ''; + var ctx = { + loginId: ctxValue('data-context-loginid'), + mobile: ctxValue('data-context-mobile') + }; $items.each(function () { var $li = $(this); var rule = RULES[$li.attr('data-rule')]; @@ -76,15 +111,29 @@ if (pw.length === 0) { $li.addClass('is-idle'); } else { - $li.addClass(rule(pw) ? 'is-pass' : 'is-fail'); + $li.addClass(rule(pw, ctx) ? 'is-pass' : 'is-fail'); } }); } + // 컨텍스트 소스 input 이 직접 타이핑되는 경우도 즉시 반영 + ['data-context-loginid', 'data-context-mobile'].forEach(function (attr) { + var id = $list.attr(attr); + if (id && document.getElementById(id)) { + $(document.getElementById(id)).on('input.passwordPolicy change.passwordPolicy', update); + } + }); + + updaters.push(update); $input.on('input.passwordPolicy', update); update(); } + // hidden input 등 이벤트 없이 값이 세팅되는 컨텍스트 변경 후 수동 재판정 + function refresh() { + updaters.forEach(function (u) { u(); }); + } + // 마크업 구동 자동 초기화 function init(root) { var $root = root ? $(root) : $(document); @@ -101,7 +150,8 @@ RULES: RULES, isValid: isValid, bind: bind, - init: init + init: init, + refresh: refresh }; $(function () { diff --git a/src/main/resources/static/js/popup/custom-popups.js b/src/main/resources/static/js/popup/custom-popups.js index 348a352..79c36f4 100644 --- a/src/main/resources/static/js/popup/custom-popups.js +++ b/src/main/resources/static/js/popup/custom-popups.js @@ -407,8 +407,12 @@ const customPopups = { return; } + // 하이픈 유무 무관 입력을 저장 표준(010-1234-5678)으로 통일해 전달 + const formattedMobile = mobile.replace(/-/g, '') + .replace(/^(01[016-9])(\d{3,4})(\d{4})$/, '$1-$2-$3'); + if (typeof onConfirm === 'function') { - onConfirm(mobile, notifyConsent); + onConfirm(formattedMobile, notifyConsent); } }); diff --git a/src/main/resources/templates/views/apps/mypage/passwordChange.html b/src/main/resources/templates/views/apps/mypage/passwordChange.html index c024bc0..1700db9 100644 --- a/src/main/resources/templates/views/apps/mypage/passwordChange.html +++ b/src/main/resources/templates/views/apps/mypage/passwordChange.html @@ -69,8 +69,11 @@ class="policy-text">동일 문자 3자리 이상 반복 불가
  • 연속된 문자/숫자 3자리 이상 불가
  • +
  • 아이디(이메일) 포함 불가
  • +
  • 휴대전화 번호 포함 불가
-

※ 아이디, 휴대전화 번호는 비밀번호에 사용할 수 없습니다.

@@ -88,6 +91,52 @@ customPopups.showAlert([[${ error }]]); }) +