Files
nice-app-web/src/pages/xkeypad/xkeypad-page.tsx
Jay Sheen 1648a30844 feat: xkeypad 보안 키패드 통합 및 비밀번호 변경 기능 구현
- xkeypad 보안 키패드 라이브러리 추가
- 비밀번호 변경 페이지에 보안 키패드 적용
- RSA 암호화 기능 통합
- route 설정 및 Sentry 설정 업데이트

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 15:00:02 +09:00

561 lines
17 KiB
TypeScript

import React, { useState, useRef, useEffect } from 'react';
import {
XKeypad,
XKeypadManager,
createPasswordKeypad,
createPinKeypad,
createCardKeypad,
type XKeypadOptions,
type XKeypadResult,
type KeyType,
type ViewType,
type NumberKeyRowCount
} from '../../utils/xkeypad';
import './xkeypad-styles.css';
export const XkeypadPage: React.FC = () => {
// State for keypad options
const [keyType, setKeyType] = useState<KeyType>('qwertysmart');
const [viewType, setViewType] = useState<ViewType>('half');
const [numberKeyRowCount, setNumberKeyRowCount] = useState<NumberKeyRowCount>(3);
const [autoKeyResize, setAutoKeyResize] = useState(false);
const [isE2E, setIsE2E] = useState(false);
const [onlyMobile, setOnlyMobile] = useState(false);
const [hasPressEffect, setHasPressEffect] = useState(true);
const [useModal, setUseModal] = useState(false);
const [useOverlay, setUseOverlay] = useState(true); // 오버레이 기본 활성화
// State for results
const [passwordResult, setPasswordResult] = useState<XKeypadResult | null>(null);
const [pinResult, setPinResult] = useState<XKeypadResult | null>(null);
const [cardResult, setCardResult] = useState<XKeypadResult | null>(null);
// State for scripts loaded
const [scriptsLoaded, setScriptsLoaded] = useState(false);
// Input refs
const passwordInputRef = useRef<HTMLInputElement>(null);
const pinInputRef = useRef<HTMLInputElement>(null);
const cardNumberInputRef = useRef<HTMLInputElement>(null);
// XKeypad instances
const passwordKeypadRef = useRef<XKeypad | null>(null);
const pinKeypadRef = useRef<XKeypad | null>(null);
const cardKeypadRef = useRef<XKeypad | null>(null);
// RSA Keys
const RSA_MODULUS = "C4F7B39E2E93DB19C016C7A0C1C05B028A1D57CB9B91E13F5B7353F8FB5AC6CE6BE31ABEB8E8F7AD18B90C08F4EBC011A6A8FCE614EA879ED5B96296B969CE92923BC9BAD6FD87F00E08F529F93010EA77E40937BDAC1C866E79ACE2F2822A3ECD982F90532D5301CF90D9BF89E953A0593AB6C5F31E99B690DD582FB85F85A9";
const RSA_EXPONENT = "10001";
// Load scripts and initialize manager
useEffect(() => {
const initializeKeypadManager = async () => {
try {
const manager = XKeypadManager.getInstance({
modulus: RSA_MODULUS,
exponent: RSA_EXPONENT
});
await manager.loadScripts();
setScriptsLoaded(true);
} catch (error) {
console.error('Failed to load XKeypad scripts:', error);
}
};
initializeKeypadManager();
return () => {
// Cleanup on unmount
if (passwordKeypadRef.current) {
passwordKeypadRef.current.destroy();
}
if (pinKeypadRef.current) {
pinKeypadRef.current.destroy();
}
if (cardKeypadRef.current) {
cardKeypadRef.current.destroy();
}
};
}, []);
// Close all keypads
const closeAllKeypads = () => {
if (passwordKeypadRef.current) {
passwordKeypadRef.current.close();
}
if (pinKeypadRef.current) {
pinKeypadRef.current.close();
}
if (cardKeypadRef.current) {
cardKeypadRef.current.close();
}
};
// Handle password keypad
const handlePasswordKeypad = async () => {
if (!scriptsLoaded || !passwordInputRef.current) return;
// Close other keypads
if (pinKeypadRef.current) pinKeypadRef.current.close();
if (cardKeypadRef.current) cardKeypadRef.current.close();
// Create or get existing keypad
if (!passwordKeypadRef.current) {
passwordKeypadRef.current = createPasswordKeypad(passwordInputRef.current, {
keyType: keyType,
viewType: viewType,
numberKeyRowCount: numberKeyRowCount,
autoKeyResize: autoKeyResize,
isE2E: isE2E,
onlyMobile: onlyMobile,
hasPressEffect: hasPressEffect,
useModal: useModal,
useOverlay: useOverlay,
onInputChange: (newLength: number) => {
console.log('Password input length:', newLength);
},
onKeypadClose: () => {
console.log('Password keypad closed');
}
}, {
modulus: RSA_MODULUS,
exponent: RSA_EXPONENT
});
} else {
// Update options if they changed
passwordKeypadRef.current = new XKeypad('password', {
keyType: keyType,
viewType: viewType,
numberKeyRowCount: numberKeyRowCount,
maxInputSize: 16,
autoKeyResize: autoKeyResize,
isE2E: isE2E,
onlyMobile: onlyMobile,
hasPressEffect: hasPressEffect,
useModal: useModal,
useOverlay: useOverlay,
onInputChange: (newLength: number) => {
console.log('Password input length:', newLength);
},
onKeypadClose: () => {
console.log('Password keypad closed');
}
}, {
modulus: RSA_MODULUS,
exponent: RSA_EXPONENT
});
}
const result = await passwordKeypadRef.current.initialize(passwordInputRef.current);
if (result === 0) {
console.log('Password keypad initialized successfully');
} else if (result === -1) {
alert('지원하지 않는 기기입니다.');
}
};
// Handle PIN keypad
const handlePinKeypad = async () => {
if (!scriptsLoaded || !pinInputRef.current) return;
// Close other keypads
if (passwordKeypadRef.current) passwordKeypadRef.current.close();
if (cardKeypadRef.current) cardKeypadRef.current.close();
// Create or get existing keypad
if (!pinKeypadRef.current) {
pinKeypadRef.current = createPinKeypad(pinInputRef.current, {
viewType: viewType,
numberKeyRowCount: numberKeyRowCount,
autoKeyResize: autoKeyResize,
isE2E: isE2E,
onlyMobile: onlyMobile,
hasPressEffect: hasPressEffect,
useModal: useModal,
useOverlay: useOverlay,
onInputChange: (newLength: number) => {
console.log('PIN input length:', newLength);
if (newLength === 6 && pinKeypadRef.current && pinKeypadRef.current.isOpen()) {
pinKeypadRef.current.close();
}
},
onKeypadClose: () => {
console.log('PIN keypad closed');
}
}, {
modulus: RSA_MODULUS,
exponent: RSA_EXPONENT
});
} else {
// Update options if they changed
pinKeypadRef.current = new XKeypad('pin', {
keyType: 'number',
viewType: viewType,
numberKeyRowCount: numberKeyRowCount,
maxInputSize: 6,
autoKeyResize: autoKeyResize,
isE2E: isE2E,
onlyMobile: onlyMobile,
hasPressEffect: hasPressEffect,
useModal: useModal,
useOverlay: useOverlay,
onInputChange: (newLength: number) => {
console.log('PIN input length:', newLength);
if (newLength === 6 && pinKeypadRef.current && pinKeypadRef.current.isOpen()) {
pinKeypadRef.current.close();
}
},
onKeypadClose: () => {
console.log('PIN keypad closed');
}
}, {
modulus: RSA_MODULUS,
exponent: RSA_EXPONENT
});
}
const result = await pinKeypadRef.current.initialize(pinInputRef.current);
if (result === 0) {
console.log('PIN keypad initialized successfully');
}
};
// Handle card keypad
const handleCardKeypad = async () => {
if (!scriptsLoaded || !cardNumberInputRef.current) return;
// Close other keypads
if (passwordKeypadRef.current) passwordKeypadRef.current.close();
if (pinKeypadRef.current) pinKeypadRef.current.close();
// Create or get existing keypad
if (!cardKeypadRef.current) {
cardKeypadRef.current = createCardKeypad(cardNumberInputRef.current, {
viewType: viewType,
autoKeyResize: autoKeyResize,
isE2E: isE2E,
onlyMobile: onlyMobile,
hasPressEffect: hasPressEffect,
useModal: useModal,
useOverlay: useOverlay,
onInputChange: (newLength: number) => {
console.log('Card number input length:', newLength);
if (newLength === 16 && cardKeypadRef.current && cardKeypadRef.current.isOpen()) {
cardKeypadRef.current.close();
}
},
onKeypadClose: () => {
console.log('Card keypad closed');
}
}, {
modulus: RSA_MODULUS,
exponent: RSA_EXPONENT
});
} else {
// Update options if they changed
cardKeypadRef.current = new XKeypad('card', {
keyType: 'number',
viewType: viewType,
numberKeyRowCount: 4,
maxInputSize: 16,
autoKeyResize: autoKeyResize,
isE2E: isE2E,
onlyMobile: onlyMobile,
hasPressEffect: hasPressEffect,
useModal: useModal,
useOverlay: useOverlay,
onInputChange: (newLength: number) => {
console.log('Card number input length:', newLength);
if (newLength === 16 && cardKeypadRef.current && cardKeypadRef.current.isOpen()) {
cardKeypadRef.current.close();
}
},
onKeypadClose: () => {
console.log('Card keypad closed');
}
}, {
modulus: RSA_MODULUS,
exponent: RSA_EXPONENT
});
}
const result = await cardKeypadRef.current.initialize(cardNumberInputRef.current);
if (result === 0) {
console.log('Card keypad initialized successfully');
}
};
// Handle submit functions
const handlePasswordSubmit = () => {
if (!passwordKeypadRef.current) return;
const result = passwordKeypadRef.current.getValue();
if (result) {
setPasswordResult(result);
console.log('Password Result:', result);
alert(`Password submitted:\n${JSON.stringify(result, null, 2)}`);
}
};
const handlePinSubmit = () => {
if (!pinKeypadRef.current) return;
const result = pinKeypadRef.current.getValue();
if (result) {
setPinResult(result);
console.log('PIN Result:', result);
alert(`PIN submitted:\n${JSON.stringify(result, null, 2)}`);
}
};
const handleCardSubmit = () => {
if (!cardKeypadRef.current) return;
const result = cardKeypadRef.current.getValue();
if (result) {
setCardResult(result);
console.log('Card Result:', result);
alert(`Card Number submitted:\n${JSON.stringify(result, null, 2)}`);
}
};
const clearAll = () => {
if (passwordKeypadRef.current) {
passwordKeypadRef.current.clear();
}
if (pinKeypadRef.current) {
pinKeypadRef.current.clear();
}
if (cardKeypadRef.current) {
cardKeypadRef.current.clear();
}
if (passwordInputRef.current) passwordInputRef.current.value = '';
if (pinInputRef.current) pinInputRef.current.value = '';
if (cardNumberInputRef.current) cardNumberInputRef.current.value = '';
setPasswordResult(null);
setPinResult(null);
setCardResult(null);
};
if (!scriptsLoaded) {
return <div>Loading XKeypad scripts...</div>;
}
return (
<div className="xkeypad-demo-container">
<h1>XecureKeypad Demo (Direct JS Integration)</h1>
<div className="demo-section">
<h2>Keypad Options</h2>
<div className="option-group">
<label>
<strong>Key Type:</strong>
<div>
<label>
<input
type="radio"
value="qwertysmart"
checked={keyType === 'qwertysmart'}
onChange={(e) => setKeyType(e.target.value as KeyType)}
/>
Qwerty Smart
</label>
<label>
<input
type="radio"
value="number"
checked={keyType === 'number'}
onChange={(e) => setKeyType(e.target.value as KeyType)}
/>
Number
</label>
</div>
</label>
</div>
<div className="option-group">
<label>
<strong>View Type:</strong>
<div>
<label>
<input
type="radio"
value="half"
checked={viewType === 'half'}
onChange={(e) => setViewType(e.target.value as ViewType)}
disabled={useModal}
/>
Half (Bottom Fixed)
</label>
<label>
<input
type="radio"
value="normal"
checked={viewType === 'normal'}
onChange={(e) => setViewType(e.target.value as ViewType)}
disabled={useModal}
/>
Normal (Below Input)
</label>
</div>
</label>
</div>
<div className="option-group">
<label>
<strong>Number Key Row Count:</strong>
<div>
{[2, 3, 4].map(count => (
<label key={count}>
<input
type="radio"
value={count}
checked={numberKeyRowCount === count}
onChange={(e) => setNumberKeyRowCount(Number(e.target.value) as NumberKeyRowCount)}
/>
{count} rows
</label>
))}
</div>
</label>
</div>
<div className="option-group">
<label>
<input
type="checkbox"
checked={autoKeyResize}
onChange={(e) => setAutoKeyResize(e.target.checked)}
/>
Auto Key Resize
</label>
</div>
<div className="option-group">
<label>
<input
type="checkbox"
checked={isE2E}
onChange={(e) => setIsE2E(e.target.checked)}
/>
E2E Communication
</label>
</div>
<div className="option-group">
<label>
<input
type="checkbox"
checked={onlyMobile}
onChange={(e) => setOnlyMobile(e.target.checked)}
/>
Only Mobile
</label>
</div>
<div className="option-group">
<label>
<input
type="checkbox"
checked={hasPressEffect}
onChange={(e) => setHasPressEffect(e.target.checked)}
/>
Has Press Effect
</label>
</div>
<div className="option-group">
<label>
<input
type="checkbox"
checked={useModal}
onChange={(e) => setUseModal(e.target.checked)}
/>
Use Modal
</label>
</div>
<div className="option-group">
<label>
<input
type="checkbox"
checked={useOverlay}
onChange={(e) => setUseOverlay(e.target.checked)}
disabled={useModal}
/>
Use Overlay (Background)
</label>
</div>
</div>
<div className="demo-section">
<h2>Input Fields</h2>
<div className="input-group">
<label>Password:</label>
<div className="input-row">
<input
ref={passwordInputRef}
type="password"
placeholder="Click to enter password"
onClick={handlePasswordKeypad}
readOnly
/>
<button onClick={handlePasswordSubmit}>Submit</button>
</div>
{passwordResult && (
<pre className="result">{JSON.stringify(passwordResult, null, 2)}</pre>
)}
</div>
<div className="input-group">
<label>PIN (6 digits):</label>
<div className="input-row">
<input
ref={pinInputRef}
type="password"
placeholder="Click to enter PIN"
onClick={handlePinKeypad}
readOnly
/>
<button onClick={handlePinSubmit}>Submit</button>
</div>
{pinResult && (
<pre className="result">{JSON.stringify(pinResult, null, 2)}</pre>
)}
</div>
<div className="input-group">
<label>Card Number (16 digits):</label>
<div className="input-row">
<input
ref={cardNumberInputRef}
type="password"
placeholder="Click to enter card number"
onClick={handleCardKeypad}
readOnly
/>
<button onClick={handleCardSubmit}>Submit</button>
</div>
{cardResult && (
<pre className="result">{JSON.stringify(cardResult, null, 2)}</pre>
)}
</div>
</div>
<div className="demo-actions">
<button onClick={closeAllKeypads} className="secondary">Close All Keypads</button>
<button onClick={clearAll} className="danger">Clear All</button>
</div>
</div>
);
};
export default XkeypadPage;