Use numeric PIN keypad for launcher auth

This commit is contained in:
seo
2026-06-17 19:46:33 +09:00
parent 356b884162
commit 5275aa17c2
3 changed files with 134 additions and 17 deletions
+118 -17
View File
@@ -97,7 +97,7 @@ function showPasswordForm(onSuccess) {
shortcut.innerHTML = '';
const title = createFormHeader('비밀번호', renderIcons);
const input = createPasswordInput('blog_secret', '비밀번호');
const input = createPinInput('blog_secret', '비밀번호');
const button = createButton('확인');
const msg = document.createElement('div');
msg.id = 'failMsg';
@@ -114,7 +114,8 @@ function showPasswordForm(onSuccess) {
shortcut.appendChild(column);
const submit = async () => {
const password = input.value;
const password = onlyDigits(input.value);
input.value = password;
try {
const messageRes = await fetch(`${apiUrl}?failMessages=1`);
@@ -149,10 +150,10 @@ function showPasswordForm(onSuccess) {
}
};
const keypad = createPinKeypad([input], submit);
column.appendChild(keypad);
button.addEventListener('click', submit);
input.addEventListener('keydown', e => {
if (e.key === 'Enter') submit();
});
attachPinInputEvents(input, submit);
input.focus();
}
@@ -161,9 +162,9 @@ function showPasswordChangeForm() {
shortcut.innerHTML = '';
const title = createFormHeader('암호 변경', renderIcons);
const currentInput = createPasswordInput('current_password', '현재 암호');
const newInput = createPasswordInput('new_password', '새 암호');
const confirmInput = createPasswordInput('confirm_password', '새 암호 확인');
const currentInput = createPinInput('current_password', '현재 암호');
const newInput = createPinInput('new_password', '새 암호');
const confirmInput = createPinInput('confirm_password', '새 암호 확인');
const button = createButton('변경');
const msg = document.createElement('div');
msg.id = 'failMsg';
@@ -179,14 +180,21 @@ function showPasswordChangeForm() {
shortcut.appendChild(column);
const submit = async () => {
const currentPassword = currentInput.value;
const newPassword = newInput.value;
const confirmPassword = confirmInput.value;
const currentPassword = onlyDigits(currentInput.value);
const newPassword = onlyDigits(newInput.value);
const confirmPassword = onlyDigits(confirmInput.value);
currentInput.value = currentPassword;
newInput.value = newPassword;
confirmInput.value = confirmPassword;
if (newPassword !== confirmPassword) {
msg.textContent = '새 암호가 일치하지 않습니다.';
return;
}
if (!isDigitPin(currentPassword) || !isDigitPin(newPassword)) {
msg.textContent = '암호는 숫자만 입력할 수 있습니다.';
return;
}
try {
const res = await fetch(apiUrl, {
@@ -205,18 +213,20 @@ function showPasswordChangeForm() {
}
msg.style.color = 'red';
msg.textContent = data.message === 'password_too_short' ? '새 암호는 4자 이상이어야 합니다.' : '현재 암호가 틀립니다.';
msg.textContent = data.message === 'password_too_short'
? '새 암호는 4자리 이상이어야 합니다.'
: (data.message === 'password_digits_only' ? '암호는 숫자만 입력할 수 있습니다.' : '현재 암호가 틀립니다.');
} catch (_) {
msg.style.color = 'red';
msg.textContent = '요청을 처리하지 못했습니다.';
}
};
const keypad = createPinKeypad([currentInput, newInput, confirmInput], submit);
column.insertBefore(keypad, msg);
button.addEventListener('click', submit);
[currentInput, newInput, confirmInput].forEach(input => {
input.addEventListener('keydown', e => {
if (e.key === 'Enter') submit();
});
attachPinInputEvents(input, submit);
});
currentInput.focus();
}
@@ -350,15 +360,106 @@ function createFormHeader(text, onBack) {
return header;
}
function createPasswordInput(id, placeholder) {
function onlyDigits(value) {
return String(value || '').replace(/\D+/g, '');
}
function isDigitPin(value) {
return /^\d+$/.test(String(value || ''));
}
function createPinInput(id, placeholder) {
const input = document.createElement('input');
input.type = 'password';
input.placeholder = placeholder;
input.id = id;
input.autocomplete = 'current-password';
input.inputMode = 'numeric';
input.pattern = '[0-9]*';
input.autocomplete = 'off';
input.enterKeyHint = 'done';
input.readOnly = true;
input.className = 'launcher-pin-input';
input.setAttribute('aria-label', placeholder);
return input;
}
function attachPinInputEvents(input, onEnter) {
input.addEventListener('focus', () => {
document.querySelectorAll('.launcher-pin-input').forEach(el => el.classList.toggle('active', el === input));
});
input.addEventListener('input', () => {
input.value = onlyDigits(input.value);
});
input.addEventListener('paste', e => {
e.preventDefault();
input.value += onlyDigits(e.clipboardData?.getData('text') || '');
});
input.addEventListener('keydown', e => {
if (/^\d$/.test(e.key)) {
e.preventDefault();
input.value += e.key;
return;
}
if (e.key === 'Backspace') {
e.preventDefault();
input.value = input.value.slice(0, -1);
return;
}
if (e.key === 'Delete') {
e.preventDefault();
input.value = '';
return;
}
if (e.key === 'Enter') {
e.preventDefault();
onEnter();
}
});
}
function createPinKeypad(inputs, onEnter) {
let activeInput = inputs[0];
const keypad = document.createElement('div');
keypad.className = 'launcher-pin-keypad';
inputs.forEach(input => {
input.addEventListener('focus', () => {
activeInput = input;
});
input.addEventListener('click', () => {
activeInput = input;
input.focus();
});
});
['1', '2', '3', '4', '5', '6', '7', '8', '9', 'clear', '0', 'back'].forEach(key => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = `launcher-pin-key ${key === 'clear' || key === 'back' ? 'launcher-pin-action' : ''}`;
btn.textContent = key === 'clear' ? 'C' : (key === 'back' ? '⌫' : key);
btn.setAttribute('aria-label', key === 'clear' ? '전체 지우기' : (key === 'back' ? '한 글자 지우기' : `${key} 입력`));
btn.addEventListener('click', () => {
activeInput.focus();
if (key === 'clear') {
activeInput.value = '';
} else if (key === 'back') {
activeInput.value = activeInput.value.slice(0, -1);
} else {
activeInput.value += key;
}
});
keypad.appendChild(btn);
});
const done = document.createElement('button');
done.type = 'button';
done.className = 'launcher-pin-key launcher-pin-done';
done.textContent = '확인';
done.addEventListener('click', onEnter);
keypad.appendChild(done);
return keypad;
}
function createButton(text) {
const button = document.createElement('button');
button.type = 'button';