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:
298
src/pages/xkeypad/xkeypad-demo.css
Normal file
298
src/pages/xkeypad/xkeypad-demo.css
Normal file
@@ -0,0 +1,298 @@
|
||||
/* XKeypad Demo Styles */
|
||||
|
||||
.xkeypad-demo-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
}
|
||||
|
||||
.xkeypad-demo-container h1 {
|
||||
color: #333;
|
||||
border-bottom: 2px solid #4CAF50;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.xkeypad-demo-container h2 {
|
||||
color: #555;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.xkeypad-demo-container h3 {
|
||||
color: #666;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* Configuration Section */
|
||||
.config-section {
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.config-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.config-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.config-group strong {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.radio-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.radio-group input[type="radio"] {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Input Section */
|
||||
.input-section {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.input-group input[type="password"],
|
||||
.input-group input[type="text"] {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 12px 15px;
|
||||
font-size: 16px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.input-group input[type="password"]:hover,
|
||||
.input-group input[type="text"]:hover {
|
||||
border-color: #4CAF50;
|
||||
}
|
||||
|
||||
.input-group input[type="password"]:focus,
|
||||
.input-group input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: #4CAF50;
|
||||
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.submit-btn {
|
||||
margin-top: 10px;
|
||||
padding: 10px 20px;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.submit-btn:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 12px 30px;
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: #1976D2;
|
||||
}
|
||||
|
||||
.control-btn.clear-btn {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
.control-btn.clear-btn:hover {
|
||||
background: #d32f2f;
|
||||
}
|
||||
|
||||
/* Result Box */
|
||||
.result-box {
|
||||
margin-top: 15px;
|
||||
padding: 15px;
|
||||
background: #e8f5e9;
|
||||
border: 1px solid #4CAF50;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.result-box h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.result-box p {
|
||||
margin: 5px 0;
|
||||
word-break: break-all;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.result-box .encrypted {
|
||||
color: #d32f2f;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Info Section */
|
||||
.info-section {
|
||||
background: #fff3e0;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ffb74d;
|
||||
}
|
||||
|
||||
.info-section ul {
|
||||
margin: 0;
|
||||
padding-left: 25px;
|
||||
}
|
||||
|
||||
.info-section li {
|
||||
margin: 10px 0;
|
||||
color: #555;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Keypad Container Styles */
|
||||
[id^="xk-pad-"] {
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.xkeypad-demo-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.input-group input[type="password"],
|
||||
.input-group input[type="text"] {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #4CAF50;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Error Message */
|
||||
.error-message {
|
||||
padding: 10px;
|
||||
background: #ffebee;
|
||||
border: 1px solid #ef5350;
|
||||
border-radius: 4px;
|
||||
color: #c62828;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Success Message */
|
||||
.success-message {
|
||||
padding: 10px;
|
||||
background: #e8f5e9;
|
||||
border: 1px solid #66bb6a;
|
||||
border-radius: 4px;
|
||||
color: #2e7d32;
|
||||
margin-top: 10px;
|
||||
}
|
||||
561
src/pages/xkeypad/xkeypad-page.tsx
Normal file
561
src/pages/xkeypad/xkeypad-page.tsx
Normal file
@@ -0,0 +1,561 @@
|
||||
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;
|
||||
314
src/pages/xkeypad/xkeypad-sample.css
Normal file
314
src/pages/xkeypad/xkeypad-sample.css
Normal file
@@ -0,0 +1,314 @@
|
||||
/* XKeypad Sample Page Styles */
|
||||
.xkeypad-sample-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||
}
|
||||
|
||||
.sample-card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
animation: fadeInUp 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.sample-card h1 {
|
||||
color: #2d3748;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 12px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #718096;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.password-form {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
color: #4a5568;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
width: 100%;
|
||||
padding: 14px 45px 14px 16px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
background: #f7fafc;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.password-input:hover {
|
||||
border-color: #cbd5e0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.password-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 20px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
display: block;
|
||||
color: #a0aec0;
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 14px 24px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.submit-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: #f7fafc;
|
||||
color: #4a5568;
|
||||
border: 2px solid #e2e8f0;
|
||||
padding: 14px 24px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
background: #e2e8f0;
|
||||
border-color: #cbd5e0;
|
||||
}
|
||||
|
||||
/* Result Section */
|
||||
.result-section {
|
||||
background: #f0f7ff;
|
||||
border: 1px solid #bee3f8;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.result-section h3 {
|
||||
color: #2b6cb6;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 15px 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.result-item .label {
|
||||
color: #718096;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.result-item .value {
|
||||
color: #2d3748;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result-item .value.encrypted {
|
||||
color: #667eea;
|
||||
word-break: break-all;
|
||||
text-align: right;
|
||||
max-width: 300px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Info Section */
|
||||
.info-section {
|
||||
background: #faf5ff;
|
||||
border: 1px solid #e9d8fd;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.info-section h3 {
|
||||
color: #553c9a;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 15px 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.info-section ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-section li {
|
||||
color: #44337a;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #e9d8fd;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-section li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.loading p {
|
||||
color: #4a5568;
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
animation: pulse 1.5s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 600px) {
|
||||
.sample-card {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.sample-card h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
font-size: 16px; /* iOS zoom 방지 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 키패드가 열릴 때 스크롤 방지 */
|
||||
body.keypad-open {
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
219
src/pages/xkeypad/xkeypad-sample.tsx
Normal file
219
src/pages/xkeypad/xkeypad-sample.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { createPasswordKeypad, XKeypad, XKeypadManager, type XKeypadResult } from '../../utils/xkeypad';
|
||||
import './xkeypad-sample.css';
|
||||
|
||||
export const XkeypadSample: React.FC = () => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [result, setResult] = useState<XKeypadResult | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [showResult, setShowResult] = useState(false);
|
||||
|
||||
const passwordInputRef = useRef<HTMLInputElement>(null);
|
||||
const passwordKeypadRef = useRef<XKeypad | null>(null);
|
||||
|
||||
// RSA Keys (실제 프로덕션에서는 서버에서 받아와야 함)
|
||||
const RSA_MODULUS = "C4F7B39E2E93DB19C016C7A0C1C05B028A1D57CB9B91E13F5B7353F8FB5AC6CE6BE31ABEB8E8F7AD18B90C08F4EBC011A6A8FCE614EA879ED5B96296B969CE92923BC9BAD6FD87F00E08F529F93010EA77E40937BDAC1C866E79ACE2F2822A3ECD982F90532D5301CF90D9BF89E953A0593AB6C5F31E99B690DD582FB85F85A9";
|
||||
const RSA_EXPONENT = "10001";
|
||||
|
||||
// 키패드 초기화
|
||||
useEffect(() => {
|
||||
const initializeKeypad = async () => {
|
||||
try {
|
||||
const manager = XKeypadManager.getInstance({
|
||||
modulus: RSA_MODULUS,
|
||||
exponent: RSA_EXPONENT
|
||||
});
|
||||
|
||||
await manager.loadScripts();
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to load XKeypad:', error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initializeKeypad();
|
||||
|
||||
return () => {
|
||||
// Cleanup
|
||||
if (passwordKeypadRef.current) {
|
||||
passwordKeypadRef.current.destroy();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 비밀번호 입력 처리
|
||||
const handlePasswordInput = async () => {
|
||||
if (!passwordInputRef.current || isLoading) return;
|
||||
|
||||
// 키패드가 없으면 생성
|
||||
if (!passwordKeypadRef.current) {
|
||||
passwordKeypadRef.current = createPasswordKeypad(passwordInputRef.current, {
|
||||
keyType: 'qwertysmart',
|
||||
viewType: 'half',
|
||||
maxInputSize: 16,
|
||||
useOverlay: true, // 오버레이 사용
|
||||
useModal: false,
|
||||
hasPressEffect: true,
|
||||
onInputChange: (length: number) => {
|
||||
console.log('Password length:', length);
|
||||
},
|
||||
onKeypadClose: () => {
|
||||
console.log('Keypad closed');
|
||||
// 키패드가 닫힐 때 입력값 업데이트
|
||||
if (passwordInputRef.current) {
|
||||
setPassword(passwordInputRef.current.value);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
modulus: RSA_MODULUS,
|
||||
exponent: RSA_EXPONENT
|
||||
});
|
||||
}
|
||||
|
||||
const result = await passwordKeypadRef.current.initialize(passwordInputRef.current);
|
||||
|
||||
if (result === 0) {
|
||||
console.log('Keypad opened successfully');
|
||||
} else if (result === -1) {
|
||||
alert('지원하지 않는 기기입니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 비밀번호 제출
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!passwordKeypadRef.current) {
|
||||
alert('비밀번호를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const keypadResult = passwordKeypadRef.current.getValue();
|
||||
|
||||
if (!keypadResult || !keypadResult.plainText) {
|
||||
alert('비밀번호를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 결과 저장 및 표시
|
||||
setResult(keypadResult);
|
||||
setShowResult(true);
|
||||
|
||||
console.log('Password submitted:', keypadResult);
|
||||
|
||||
// 서버로 전송할 데이터
|
||||
const dataToSend = {
|
||||
encryptedPassword: keypadResult.rsaEncrypted || keypadResult.plainText,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
console.log('Data to send to server:', dataToSend);
|
||||
|
||||
// TODO: 서버로 전송
|
||||
// await sendToServer(dataToSend);
|
||||
};
|
||||
|
||||
// 초기화
|
||||
const handleReset = () => {
|
||||
if (passwordKeypadRef.current) {
|
||||
passwordKeypadRef.current.clear();
|
||||
}
|
||||
|
||||
if (passwordInputRef.current) {
|
||||
passwordInputRef.current.value = '';
|
||||
}
|
||||
|
||||
setPassword('');
|
||||
setResult(null);
|
||||
setShowResult(false);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="xkeypad-sample-container">
|
||||
<div className="loading">
|
||||
<p>키패드 로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="xkeypad-sample-container">
|
||||
<div className="sample-card">
|
||||
<h1>보안 키패드 샘플</h1>
|
||||
<p className="description">
|
||||
보안 키패드를 사용하여 안전하게 비밀번호를 입력하세요.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit} className="password-form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">비밀번호</label>
|
||||
<div className="input-wrapper">
|
||||
<input
|
||||
ref={passwordInputRef}
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="클릭하여 비밀번호 입력"
|
||||
onClick={handlePasswordInput}
|
||||
readOnly
|
||||
className="password-input"
|
||||
/>
|
||||
<span className="input-icon">🔒</span>
|
||||
</div>
|
||||
<small className="help-text">
|
||||
입력 필드를 클릭하면 보안 키패드가 나타납니다.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="button-group">
|
||||
<button type="submit" className="submit-btn">
|
||||
로그인
|
||||
</button>
|
||||
<button type="button" onClick={handleReset} className="reset-btn">
|
||||
초기화
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{showResult && result && (
|
||||
<div className="result-section">
|
||||
<h3>입력 결과</h3>
|
||||
<div className="result-content">
|
||||
<div className="result-item">
|
||||
<span className="label">Type:</span>
|
||||
<span className="value">{result.type}</span>
|
||||
</div>
|
||||
{result.plainText && (
|
||||
<div className="result-item">
|
||||
<span className="label">Plain Text:</span>
|
||||
<span className="value">{'•'.repeat(result.plainText.length)}</span>
|
||||
</div>
|
||||
)}
|
||||
{result.rsaEncrypted && (
|
||||
<div className="result-item">
|
||||
<span className="label">RSA Encrypted:</span>
|
||||
<span className="value encrypted">{result.rsaEncrypted.substring(0, 50)}...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="info-section">
|
||||
<h3>특징</h3>
|
||||
<ul>
|
||||
<li>✅ RSA 암호화 지원</li>
|
||||
<li>✅ 가상 키패드로 키로거 방지</li>
|
||||
<li>✅ 랜덤 키 배열</li>
|
||||
<li>✅ 모바일 최적화</li>
|
||||
<li>✅ 터치/클릭 이펙트</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default XkeypadSample;
|
||||
250
src/pages/xkeypad/xkeypad-styles.css
Normal file
250
src/pages/xkeypad/xkeypad-styles.css
Normal file
@@ -0,0 +1,250 @@
|
||||
/* XKeypad Demo Styles */
|
||||
|
||||
.xkeypad-demo-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.xkeypad-demo-container h1 {
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #007bff;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.demo-section h2 {
|
||||
color: #495057;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.option-group {
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.option-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.option-group label strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.option-group label input[type="radio"] {
|
||||
margin-right: 8px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.option-group label input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.option-group > label > div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.option-group > label > div > label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.input-group > label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-row input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-row input:hover {
|
||||
border-color: #80bdff;
|
||||
}
|
||||
|
||||
.input-row input:focus {
|
||||
outline: none;
|
||||
border-color: #80bdff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
|
||||
}
|
||||
|
||||
.input-row button {
|
||||
padding: 10px 20px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input-row button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.demo-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.demo-actions button {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.demo-actions button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.demo-actions button.secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.demo-actions button.danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.xkeypad-modal-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9998;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.xkeypad-modal-wrapper.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.xkeypad-modal-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9998;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.xkeypad-modal-wrapper.show .xkeypad-modal-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.xkeypad-modal-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
||||
z-index: 9999;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s ease;
|
||||
padding: 20px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.xkeypad-modal-wrapper.show .xkeypad-modal-container {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.xkeypad-modal-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.xkeypad-demo-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.input-row input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-row button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.demo-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-actions button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user