API 스펙 관리 문구 및 로직 개선
- 게이트웨이 관련 문구 수정 및 그룹 설정 안내 추가 - 비 로그인 사용자 공개 관련 문구 수정 - description 필드 저장/복원 로직 추가
This commit is contained in:
@@ -2,8 +2,10 @@
|
||||
* 비밀번호 문자열 정책 라이브 검증 (공용)
|
||||
*
|
||||
* 서버 검증기 PasswordRuleValidator.isValid(= @PasswordRule) 의
|
||||
* "문자열" 규칙을 그대로 클라이언트로 포팅한다. 아이디/휴대전화 포함 여부는
|
||||
* 민감정보 노출을 피하기 위해 서버 검증에만 맡긴다.
|
||||
* 규칙을 클라이언트로 포팅한다. 아이디/휴대전화 포함 규칙(noid/nomobile)은
|
||||
* 사용자가 폼에 직접 입력한 값이 페이지에 이미 있을 때만 ul 의
|
||||
* data-context-loginid/data-context-mobile 로 연결해 쓴다 — DB 값을 새로
|
||||
* 내려받아야 하는 화면(비밀번호 변경)은 서버 AJAX(/password/content-check)로 판정한다.
|
||||
*
|
||||
* 사용법(마크업 구동):
|
||||
* <ul class="password-policy-checklist" data-password-input="newPassword">
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -69,8 +69,11 @@
|
||||
class="policy-text">동일 문자 3자리 이상 반복 불가</span></li>
|
||||
<li data-rule="noseq" class="is-idle"><span class="policy-icon"></span><span
|
||||
class="policy-text">연속된 문자/숫자 3자리 이상 불가</span></li>
|
||||
<li data-rule="noid-server" class="is-idle"><span class="policy-icon"></span><span
|
||||
class="policy-text">아이디(이메일) 포함 불가</span></li>
|
||||
<li data-rule="nomobile-server" class="is-idle"><span class="policy-icon"></span><span
|
||||
class="policy-text">휴대전화 번호 포함 불가</span></li>
|
||||
</ul>
|
||||
<p class="password-policy-note">※ 아이디, 휴대전화 번호는 비밀번호에 사용할 수 없습니다.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-actions" style="justify-content: flex-end;">
|
||||
@@ -88,6 +91,52 @@
|
||||
customPopups.showAlert([[${ error }]]);
|
||||
})
|
||||
</script>
|
||||
<script th:inline="javascript">
|
||||
// 아이디/휴대전화 포함 여부 라이브 체크 — 민감정보를 페이지에 내리지 않고
|
||||
// 서버(/password/content-check, 세션 사용자 기준)로 판정한다. data-rule 이
|
||||
// RULES 에 없는 *-server 항목은 password-policy.js 가 건드리지 않는다.
|
||||
(function () {
|
||||
var input = document.getElementById('newPassword');
|
||||
var liId = document.querySelector('li[data-rule="noid-server"]');
|
||||
var liMobile = document.querySelector('li[data-rule="nomobile-server"]');
|
||||
if (!input || !liId || !liMobile) return;
|
||||
|
||||
function setState(li, state) {
|
||||
li.classList.remove('is-idle', 'is-pass', 'is-fail');
|
||||
li.classList.add(state);
|
||||
}
|
||||
|
||||
var csrfToken = document.querySelector('meta[name="_csrf"]');
|
||||
var csrfHeader = document.querySelector('meta[name="_csrf_header"]');
|
||||
var timer = null;
|
||||
|
||||
input.addEventListener('input', function () {
|
||||
var pw = input.value;
|
||||
if (timer) clearTimeout(timer);
|
||||
if (!pw) {
|
||||
setState(liId, 'is-idle');
|
||||
setState(liMobile, 'is-idle');
|
||||
return;
|
||||
}
|
||||
timer = setTimeout(function () {
|
||||
var headers = {};
|
||||
if (csrfToken && csrfHeader) {
|
||||
headers[csrfHeader.content] = csrfToken.content;
|
||||
}
|
||||
$.ajax({
|
||||
url: /*[[@{/password/content-check}]]*/ '/password/content-check',
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
data: { password: pw }
|
||||
}).done(function (res) {
|
||||
if (input.value !== pw) return; // 입력이 이미 바뀐 응답은 무시
|
||||
setState(liId, res.idIncluded ? 'is-fail' : 'is-pass');
|
||||
setState(liMobile, res.mobileIncluded ? 'is-fail' : 'is-pass');
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script th:inline="javascript">
|
||||
// 반영 직전 2FA: twofaRequired 면 제출을 가로채 2FA 팝업 → 성공 시 실제 제출.
|
||||
(function () {
|
||||
|
||||
@@ -402,16 +402,17 @@
|
||||
const last = document.getElementById('newMobileLast')?.value.trim();
|
||||
|
||||
if (prefix === '선택' || !middle || !last) return null;
|
||||
return prefix + middle + last;
|
||||
// 저장 표준(하이픈 정규형)에 맞춰 조합
|
||||
return `${prefix}-${middle}-${last}`;
|
||||
},
|
||||
|
||||
// 기존 휴대폰 번호 가져오기 (하이픈 없이)
|
||||
// 기존 휴대폰 번호 가져오기 (하이픈 정규형)
|
||||
getExistingMobileNumber: () => {
|
||||
const prefix = document.querySelector('[name="mobilePrefix"]')?.value;
|
||||
const middle = document.querySelector('[name="mobileMiddle"]')?.value;
|
||||
const last = document.querySelector('[name="mobileLast"]')?.value;
|
||||
|
||||
return prefix + middle + last;
|
||||
return `${prefix}-${middle}-${last}`;
|
||||
},
|
||||
|
||||
// 휴대폰 번호 변경 여부 확인
|
||||
|
||||
@@ -77,7 +77,8 @@
|
||||
const originalPrefix = document.querySelector('input[name="mobilePrefix"]').value;
|
||||
const originalMiddle = document.querySelector('input[name="mobileMiddle"]').value;
|
||||
const originalLast = document.querySelector('input[name="mobileLast"]').value;
|
||||
const originalMobileNumber = `${originalPrefix}${originalMiddle}${originalLast}`;
|
||||
// 저장 표준(하이픈 정규형)에 맞춰 조합 — 미변경 제출 시에도 이 값이 그대로 전송된다
|
||||
const originalMobileNumber = `${originalPrefix}-${originalMiddle}-${originalLast}`;
|
||||
|
||||
// 현재 값들 가져오기
|
||||
const currentName = document.querySelector('input[name="userName"]').value.trim();
|
||||
@@ -103,7 +104,7 @@
|
||||
|
||||
// 하이픈 제거 후 비교
|
||||
const newMobileRaw = newMobileNumber.replace(/-/g, '');
|
||||
if (newMobileRaw !== originalMobileNumber) {
|
||||
if (newMobileRaw !== originalMobileNumber.replace(/-/g, '')) {
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,8 @@
|
||||
th:placeholder="#{portalUser.Register.pass}">
|
||||
<input type="hidden" name="isPasswordValid" id="isPasswordValid" />
|
||||
<div id="password-validation" class="org-validation-message"></div>
|
||||
<ul class="password-policy-checklist" data-password-input="password">
|
||||
<ul class="password-policy-checklist" data-password-input="password"
|
||||
data-context-loginid="loginId" data-context-mobile="mobileNumber">
|
||||
<li data-rule="length" class="is-idle"><span class="policy-icon"></span><span class="policy-text">영문/숫자/특수문자
|
||||
포함 8~50자</span></li>
|
||||
<li data-rule="letter" class="is-idle"><span class="policy-icon"></span><span class="policy-text">영문
|
||||
@@ -95,8 +96,11 @@
|
||||
3자리 이상 반복 불가</span></li>
|
||||
<li data-rule="noseq" class="is-idle"><span class="policy-icon"></span><span class="policy-text">연속된 문자/숫자
|
||||
3자리 이상 불가</span></li>
|
||||
<li data-rule="noid" class="is-idle"><span class="policy-icon"></span><span class="policy-text">아이디(이메일)
|
||||
포함 불가</span></li>
|
||||
<li data-rule="nomobile" class="is-idle"><span class="policy-icon"></span><span class="policy-text">휴대전화 번호
|
||||
포함 불가</span></li>
|
||||
</ul>
|
||||
<p class="password-policy-note">※ 아이디, 휴대전화 번호는 비밀번호에 사용할 수 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,6 +145,10 @@
|
||||
if (mobileNumberInput) {
|
||||
mobileNumberInput.value = formattedNumber;
|
||||
}
|
||||
// hidden 값 변경은 input 이벤트가 없으므로 체크리스트(nomobile) 수동 재판정
|
||||
if (window.PasswordPolicy) {
|
||||
PasswordPolicy.refresh();
|
||||
}
|
||||
return formattedNumber;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<input type="tel"
|
||||
id="userInviteMobileInput"
|
||||
class="pop_input_field"
|
||||
placeholder="휴대폰 번호 ('-' 없이 입력)"
|
||||
placeholder="휴대폰 번호 (예: 010-1234-5678)"
|
||||
maxlength="13">
|
||||
<div id="userInvitePopupError" class="error-message"></div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user