feat: xkeypad 보안 키패드 통합 및 비밀번호 변경 기능 구현

- xkeypad 보안 키패드 라이브러리 추가
- 비밀번호 변경 페이지에 보안 키패드 적용
- RSA 암호화 기능 통합
- route 설정 및 Sentry 설정 업데이트

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jay Sheen
2025-10-21 14:59:07 +09:00
parent ab5bea6aeb
commit 1648a30844
41 changed files with 3426 additions and 11 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useRef, useEffect } from 'react';
import { PATHS } from '@/shared/constants/paths';
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { HeaderType } from '@/entities/common/model/types';
@@ -11,6 +11,7 @@ import {
import { useUserChangeCancelPasswordMutation } from '@/entities/user/api/use-user-change-cancel-password-mutation';
import { snackBar } from '@/shared/lib/toast';
import { useStore } from '@/shared/model/store';
import { XKeypad, XKeypadManager, createPasswordKeypad } from '@/utils/xkeypad';
export const PasswordModifyCancelPasswordPage = () => {
const { navigate } = useNavigate();
@@ -20,13 +21,30 @@ export const PasswordModifyCancelPasswordPage = () => {
const [mid, setMid] = useState<string>(userMid);
const [password, setPassword] = useState<string>('');
const [confirmPassword, setConfirmPassword] = useState<string>('');
const [isKeypadLoaded, setIsKeypadLoaded] = useState(false);
// Input refs for xkeypad
const passwordInputRef = useRef<HTMLInputElement>(null);
const confirmPasswordInputRef = useRef<HTMLInputElement>(null);
// XKeypad instances
const passwordKeypadRef = useRef<XKeypad | null>(null);
const confirmPasswordKeypadRef = useRef<XKeypad | null>(null);
// RSA Keys (실제 프로덕션에서는 서버에서 받아와야 함)
const RSA_MODULUS = "C4F7B39E2E93DB19C016C7A0C1C05B028A1D57CB9B91E13F5B7353F8FB5AC6CE6BE31ABEB8E8F7AD18B90C08F4EBC011A6A8FCE614EA879ED5B96296B969CE92923BC9BAD6FD87F00E08F529F93010EA77E40937BDAC1C866E79ACE2F2822A3ECD982F90532D5301CF90D9BF89E953A0593AB6C5F31E99B690DD582FB85F85A9";
const RSA_EXPONENT = "10001";
const changeCancelPasswordMutation = useUserChangeCancelPasswordMutation({
onSuccess: () => {
snackBar('비밀번호가 성공적으로 변경되었습니다.');
// Clear form
// Clear form and keypads
setPassword('');
setConfirmPassword('');
if (passwordKeypadRef.current) passwordKeypadRef.current.clear();
if (confirmPasswordKeypadRef.current) confirmPasswordKeypadRef.current.clear();
if (passwordInputRef.current) passwordInputRef.current.value = '';
if (confirmPasswordInputRef.current) confirmPasswordInputRef.current.value = '';
// Navigate back
navigate(PATHS.account.password.manage);
},
@@ -42,6 +60,39 @@ export const PasswordModifyCancelPasswordPage = () => {
{ value: 'nictest02m', label: 'nictest02m' },
];
// Initialize XKeypad
useEffect(() => {
const initializeKeypad = async () => {
try {
const manager = XKeypadManager.getInstance({
modulus: RSA_MODULUS,
exponent: RSA_EXPONENT
});
await manager.loadScripts();
// RSA 키 설정을 명시적으로 다시 한번 수행
manager.setRSAPublicKey(RSA_MODULUS, RSA_EXPONENT);
setIsKeypadLoaded(true);
} catch (error) {
console.error('Failed to load XKeypad:', error);
}
};
initializeKeypad();
return () => {
// Cleanup keypads on unmount
if (passwordKeypadRef.current) {
passwordKeypadRef.current.destroy();
}
if (confirmPasswordKeypadRef.current) {
confirmPasswordKeypadRef.current.destroy();
}
};
}, []);
useSetHeaderTitle('거래취소 비밀번호 변경');
useSetHeaderType(HeaderType.LeftArrow);
useSetFooterMode(false);
@@ -58,11 +109,97 @@ export const PasswordModifyCancelPasswordPage = () => {
);
};
// Handle password keypad
const handlePasswordKeypad = async () => {
if (!passwordInputRef.current || !isKeypadLoaded) return;
// Close other keypad if open
if (confirmPasswordKeypadRef.current) {
confirmPasswordKeypadRef.current.close();
}
// Create or initialize password keypad
if (!passwordKeypadRef.current) {
passwordKeypadRef.current = createPasswordKeypad(passwordInputRef.current, {
keyType: 'qwertysmart',
viewType: 'half',
maxInputSize: 16,
useOverlay: true,
useModal: false,
hasPressEffect: true,
isE2E: false, // E2E 모드 비활성화
onInputChange: (length: number) => {
// Update password state as typing
if (passwordKeypadRef.current) {
const plainText = passwordKeypadRef.current.getPlainText();
console.log('passwordKeypadRef:', plainText, passwordInputRef.current?.value);
setPassword(plainText);
}
},
onKeypadClose: () => {
// Final update when keypad closes
if (passwordKeypadRef.current) {
const plainText = passwordKeypadRef.current.getPlainText();
setPassword(plainText);
}
}
});
}
const result = await passwordKeypadRef.current.initialize(passwordInputRef.current);
if (result !== 0) {
console.error('Failed to initialize password keypad');
}
};
// Handle confirm password keypad
const handleConfirmPasswordKeypad = async () => {
if (!confirmPasswordInputRef.current || !isKeypadLoaded) return;
// Close other keypad if open
if (passwordKeypadRef.current) {
passwordKeypadRef.current.close();
}
// Create or initialize confirm password keypad
if (!confirmPasswordKeypadRef.current) {
confirmPasswordKeypadRef.current = createPasswordKeypad(confirmPasswordInputRef.current, {
keyType: 'qwertysmart',
viewType: 'half',
maxInputSize: 16,
useOverlay: true,
useModal: false,
hasPressEffect: true,
isE2E: false, // E2E 모드 비활성화
onInputChange: (length: number) => {
// Update confirm password state as typing
if (confirmPasswordKeypadRef.current) {
const plainText = confirmPasswordKeypadRef.current.getPlainText();
console.log('confirmPasswordKeypadRef:', plainText, confirmPasswordInputRef.current?.value);
setConfirmPassword(plainText);
}
},
onKeypadClose: () => {
// Final update when keypad closes
if (confirmPasswordKeypadRef.current) {
const plainText = confirmPasswordKeypadRef.current.getPlainText();
setConfirmPassword(plainText);
}
}
});
}
const result = await confirmPasswordKeypadRef.current.initialize(confirmPasswordInputRef.current);
if (result !== 0) {
console.error('Failed to initialize confirm password keypad');
}
};
// 저장 버튼 클릭 핸들러
const handleSave = () => {
if (!isFormValid()) return;
// TODO: Validate current password before submitting
// 평문 비밀번호 사용 (E2E 모드가 꺼져있으므로)
changeCancelPasswordMutation.mutate({
mid,
password: password
@@ -96,21 +233,27 @@ export const PasswordModifyCancelPasswordPage = () => {
<div className="ua-row">
<div className="ua-label"> <span className="red">*</span></div>
<input
ref={passwordInputRef}
className={`wid-100 ${confirmPassword && password !== confirmPassword ? 'error' : ''}`}
type="password"
placeholder=""
placeholder="클릭하여 비밀번호 입력"
value={password}
onChange={(e) => setPassword(e.target.value)}
onClick={handlePasswordKeypad}
readOnly
style={{ cursor: 'pointer' }}
/>
</div>
<div className="ua-row">
<div className="ua-label"> <span className="red">*</span></div>
<input
ref={confirmPasswordInputRef}
className={`wid-100 ${confirmPassword && password !== confirmPassword ? 'error' : ''}`}
type="password"
placeholder=""
placeholder="클릭하여 비밀번호 재입력"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
onClick={handleConfirmPasswordKeypad}
readOnly
style={{ cursor: 'pointer' }}
/>
</div>
{confirmPassword && password !== confirmPassword && (
@@ -118,7 +261,7 @@ export const PasswordModifyCancelPasswordPage = () => {
)}
</div>
<div className="apply-row bottom-padding">
<div className="apply-row">
<button
className="btn-50 btn-blue flex-1"
type="button"

View File

@@ -104,7 +104,7 @@ export const PasswordModifyLoginPasswordPage = () => {
)}
</div>
<div className="apply-row bottom-padding">
<div className="apply-row">
<button
className="btn-50 btn-blue flex-1"
type="button"