- xkeypad 보안 키패드 라이브러리 추가 - 비밀번호 변경 페이지에 보안 키패드 적용 - RSA 암호화 기능 통합 - route 설정 및 Sentry 설정 업데이트 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
561 lines
17 KiB
TypeScript
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; |