feat: xkeypad 보안 키패드 통합 및 비밀번호 변경 기능 구현
- xkeypad 보안 키패드 라이브러리 추가 - 비밀번호 변경 페이지에 보안 키패드 적용 - RSA 암호화 기능 통합 - route 설정 및 Sentry 설정 업데이트 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
BIN
public/images/xkeypad/blank_key.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/images/xkeypad/button.png
Executable file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/images/xkeypad/html5/bg_btm.png
Executable file
|
After Width: | Height: | Size: 124 B |
BIN
public/images/xkeypad/html5/bg_rgt.gif
Executable file
|
After Width: | Height: | Size: 49 B |
BIN
public/images/xkeypad/html5/button.gif
Executable file
|
After Width: | Height: | Size: 43 B |
BIN
public/images/xkeypad/html5/button.png
Executable file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/images/xkeypad/html5/logo_white.png
Executable file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/images/xkeypad/html5/no_logo_white.png
Executable file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/xkeypad/html5/sp_xkp_ui_white.png
Executable file
|
After Width: | Height: | Size: 84 KiB |
BIN
public/images/xkeypad/html5/sp_xkp_ui_white2.png
Executable file
|
After Width: | Height: | Size: 70 KiB |
BIN
public/images/xkeypad/html5/sp_xkp_white.png
Executable file
|
After Width: | Height: | Size: 644 B |
BIN
public/images/xkeypad/html5/xkbasebt.gif
Executable file
|
After Width: | Height: | Size: 43 B |
BIN
public/images/xkeypad/html5/xkcur.cur
Executable file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/images/xkeypad/html5/xkcur.png
Executable file
|
After Width: | Height: | Size: 316 B |
BIN
public/images/xkeypad/logo.png
Executable file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/images/xkeypad/logo_white.png
Executable file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/images/xkeypad/logo_white_html.png
Executable file
|
After Width: | Height: | Size: 730 B |
BIN
public/images/xkeypad/overlay.png
Executable file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
public/images/xkeypad/sp_xkp_white.png
Executable file
|
After Width: | Height: | Size: 29 KiB |
BIN
public/images/xkeypad/sp_xkp_white_big.png
Executable file
|
After Width: | Height: | Size: 44 KiB |
BIN
public/images/xkeypad/sp_xkp_white_origin.png
Executable file
|
After Width: | Height: | Size: 57 KiB |
BIN
public/images/xkeypad/xkbasebt.gif
Executable file
|
After Width: | Height: | Size: 43 B |
BIN
public/images/xkeypad/xkcur.cur
Executable file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/images/xkeypad/xkcur.png
Executable file
|
After Width: | Height: | Size: 316 B |
BIN
public/images/xkeypad/xkp_white_bg.png
Executable file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/images/xkeypad/xkp_white_button.png
Executable file
|
After Width: | Height: | Size: 76 KiB |
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,9 @@ const VatReturnPages = lazyLoad('/src/pages/vat-return/vat-return-pages
|
||||
const AdditionalServicePages = lazyLoad('/src/pages/additional-service/additional-service-pages');
|
||||
const SupportPages = lazyLoad('/src/pages/support/support-pages');
|
||||
const SettingPage = lazyLoad('/src/pages/setting/setting-page');
|
||||
const AlarmPages = lazyLoad('/src/pages/alarm/alarm-pages');
|
||||
const AlarmPages = lazyLoad('/src/pages/alarm/alarm-pages');
|
||||
const XkeypadPage = lazyLoad('/src/pages/xkeypad/xkeypad-page');
|
||||
const XkeypadSample = lazyLoad('/src/pages/xkeypad/xkeypad-sample');
|
||||
|
||||
export const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
|
||||
const Pages = () => {
|
||||
@@ -105,6 +107,8 @@ const Pages = () => {
|
||||
<Route path={ROUTE_NAMES.support.base} element={<SupportPages />} />
|
||||
<Route path={ROUTE_NAMES.setting} element={<SettingPage />} />
|
||||
<Route path={ROUTE_NAMES.alarm.base} element={<AlarmPages />} />
|
||||
<Route path={ROUTE_NAMES.xkeypad} element={<XkeypadPage />} />
|
||||
<Route path={ROUTE_NAMES.xkeypadSample} element={<XkeypadSample />} />
|
||||
</Route>
|
||||
<Route path="*" element={<NotFoundError />} />
|
||||
</Route>
|
||||
|
||||
@@ -162,7 +162,8 @@ export const ROUTE_NAMES = {
|
||||
base: '/alarm/*',
|
||||
list: 'list',
|
||||
},
|
||||
|
||||
xkeypad: '/xkeypad',
|
||||
xkeypadSample: '/xkeypad-sample',
|
||||
};
|
||||
|
||||
export type RouteNamesType = typeof ROUTE_NAMES;
|
||||
|
||||
206
src/shared/ui/assets/css/xkeypad-modal.css
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* XKeypad Modal Styles
|
||||
* 키패드 모달 스타일
|
||||
*/
|
||||
|
||||
/* 모달 오버레이 - 전체화면 */
|
||||
.xkeypad-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
z-index: 9998;
|
||||
display: none;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
.xkeypad-modal-overlay.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 모달 컨테이너 */
|
||||
.xkeypad-modal-container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: transparent;
|
||||
z-index: 9999;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
/* Safe area 적용 */
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
padding-bottom: constant(safe-area-inset-bottom, 0); /* iOS 11.0 */
|
||||
}
|
||||
|
||||
.xkeypad-modal-container.active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 키패드 래퍼 */
|
||||
.xkeypad-wrapper {
|
||||
position: relative;
|
||||
background-color: #dbdde2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 키패드 컨텐츠 영역 */
|
||||
.xkeypad-content {
|
||||
position: relative;
|
||||
background-color: #dbdde2;
|
||||
}
|
||||
|
||||
/* XKeypad 컨테이너가 모달 내에서 보이도록 */
|
||||
.xkeypad-content .xkp_ui_qwerty,
|
||||
.xkeypad-content .xkp_ui_number {
|
||||
position: relative !important;
|
||||
z-index: 10001 !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
/* 모든 키패드 자식 요소들이 보이도록 */
|
||||
.xkeypad-content .xkp_ui_qwerty *,
|
||||
.xkeypad-content .xkp_ui_number * {
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* 키패드 DIV가 모달 내에서 보이도록 */
|
||||
.xkeypad-content > div {
|
||||
position: relative !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* 키패드 버튼들이 보이도록 */
|
||||
.xkeypad-content ul,
|
||||
.xkeypad-content li,
|
||||
.xkeypad-content a {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* 애니메이션 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 반응형 대응 */
|
||||
@media (max-width: 768px) {
|
||||
.xkeypad-modal-container {
|
||||
/* 모바일에서는 전체 너비 */
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
/* 데스크톱에서는 중앙 정렬 */
|
||||
.xkeypad-modal-container {
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(100%);
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.xkeypad-modal-container.active {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* iOS 노치 대응 */
|
||||
@supports (padding: max(0px)) {
|
||||
.xkeypad-modal-container {
|
||||
padding-bottom: max(env(safe-area-inset-bottom, 0), 0px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 키패드 타입별 높이는 자동 조정 - min-height 제거 */
|
||||
|
||||
/* 입력 필드 활성화 스타일 */
|
||||
.xkeypad-input-active {
|
||||
border-color: #4CAF50 !important;
|
||||
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1) !important;
|
||||
}
|
||||
|
||||
/* 모달 열림/닫힘 시 body 스크롤 방지 */
|
||||
body.xkeypad-modal-open {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
/* top 값은 JavaScript에서 동적으로 설정 */
|
||||
}
|
||||
|
||||
/* iOS 바운스 스크롤 방지 */
|
||||
.xkeypad-modal-container {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
/* 터치 이벤트 최적화 */
|
||||
.xkeypad-modal-overlay,
|
||||
.xkeypad-modal-container {
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.xkeypad-content {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* 다크모드 지원 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.xkeypad-modal-overlay {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.xkeypad-wrapper {
|
||||
background-color: #2c2c2e;
|
||||
border-top-color: #48484a;
|
||||
}
|
||||
|
||||
.xkeypad-modal-header {
|
||||
background-color: #1c1c1e;
|
||||
border-bottom-color: #48484a;
|
||||
}
|
||||
|
||||
.xkeypad-drag-indicator {
|
||||
background-color: #636366;
|
||||
}
|
||||
|
||||
.xkeypad-close-btn {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.xkeypad-close-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.xkeypad-close-btn::before {
|
||||
color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
663
src/shared/ui/assets/css/xkeypad.css
Normal file
@@ -0,0 +1,663 @@
|
||||
/**
|
||||
* XecureKeypad Mobile Web
|
||||
* @version 1.5.0.6
|
||||
* @release 2022-12-13
|
||||
*/
|
||||
@charset "utf-8";
|
||||
|
||||
/* iPhone 글자크기 자동조정 방지 */
|
||||
html{-webkit-text-size-adjust:100%;}
|
||||
|
||||
/* 배포 코드 */
|
||||
.xkp_ui_qwerty,
|
||||
.xkp_ui_qwerty * { -moz-box-sizing: border-box; box-sizing: border-box; }
|
||||
|
||||
.xkp_ui_qwerty div,
|
||||
.xkp_ui_qwerty ul,
|
||||
.xkp_ui_qwerty li,
|
||||
.xkp_ui_qwerty table,
|
||||
.xkp_ui_qwerty th,
|
||||
.xkp_ui_qwerty td {margin:0;padding:0}
|
||||
.xkp_ui_qwerty body{-webkit-text-size-adjust:none}
|
||||
.xkp_ui_qwerty img{border:0;vertical-align:top}
|
||||
.xkp_ui_qwerty ul,.xkp_ui_qwerty li{list-style:none}
|
||||
.xkp_ui_qwerty em{font-style:normal;font-weight:normal}
|
||||
.xkp_ui_qwerty table{border-collapse:collapse;border-spacing:0}
|
||||
.xkp_ui_qwerty{margin:0;padding:5px 2px 3px;border-top: 1px solid #cfcdcd;border-bottom: 1px solid #b4b4b4;background-color:#dbdde2;font-size:20px}
|
||||
.xkp_ui_qwerty table{width:100%;table-layout:fixed}
|
||||
.xkp_ui_qwerty td{padding:0 1px 3px;vertical-align:middle}
|
||||
|
||||
.xkp_ui_qwerty .xkp_dummy{display:inline-block;height:100%;vertical-align:middle}
|
||||
.xkp_ui_qwerty .xkp_ui_tb{display:table;width:100%;table-layout:fixed}
|
||||
.xkp_ui_qwerty .xkp_ui_tb .xkp_ui_cell.xkp_first{padding-right:2px}
|
||||
.xkp_ui_qwerty .xkp_ui_tb .xkp_ui_cell.xkp_pad_n{padding:0}
|
||||
.xkp_ui_qwerty .xkp_ui_cell{display:table-cell;vertical-align:middle}
|
||||
|
||||
/* [ Qwerty & Number ] Common Border Style */
|
||||
.xkp_ui_qwerty a.xkqwerty,
|
||||
.xkp_ui_qwerty a.xkp_key2,
|
||||
.xkp_ui_qwerty a.xkp_key3,
|
||||
.xkp_ui_qwerty a.xkp_key4 { border-width: 1px; border-style: solid; border-radius: 3px; -webkit-tap-highlight-color: rgba(0,0,0,0) !important;}
|
||||
|
||||
/* [ Qwerty ] 0 ~ 9 */
|
||||
.xkp_ui_qwerty a.xkqwerty { background-color: #f9f9f9; border-color: #cdced0; }
|
||||
/* [ Qwerty & Number ] Upper/Lower Case Character & Number & Symbol On/Off */
|
||||
.xkp_ui_qwerty a.xkp_key2{ background-color: #ffffff; border-color: #cdced0; }
|
||||
/* [ Qwerty & Number ] Capslock & Refresh & Symbol On/Off & Space & Backspace */
|
||||
.xkp_ui_qwerty a.xkp_key3 { background-color: #262d39; border-color: #565D69; }
|
||||
/* [ Qwerty & Number ] Enter */
|
||||
.xkp_ui_qwerty a.xkp_key4 { background-color: #126bd8; }
|
||||
|
||||
/* [ Qwerty ] 0 ~ 9 */
|
||||
.xkp_ui_qwerty.has_press_effect a.xkqwerty:active { background-color: #AFB2B8; border-color: #9CA1AA; }
|
||||
/* [ Qwerty & Number ] Upper/Lower Case Character & Number & Symbol On/Off */
|
||||
.xkp_ui_qwerty.has_press_effect a.xkp_key2:active{ background-color: #AFB2B8; border-color: #9CA1AA; }
|
||||
/* [ Qwerty & Number ] Capslock & Refresh & Symbol On/Off & Space & Backspace */
|
||||
.xkp_ui_qwerty.has_press_effect a.xkp_key3:active{ background-color: #AFB2B8; border-color: #9CA1AA; }
|
||||
/* [ Qwerty & Number ] Enter */
|
||||
.xkp_ui_qwerty.has_press_effect a.xkp_key4:active{ background-color: #AFB2B8; border-color: #9CA1AA; }
|
||||
|
||||
/****************************
|
||||
스프라이트 이미지 설정
|
||||
*****************************/
|
||||
.xkp_ui_qwerty em{display:inline-block;overflow:hidden;color:transparent;white-space:nowrap;vertical-align:top;letter-spacing:-5px}
|
||||
|
||||
/****************************
|
||||
버튼 기본 스타일
|
||||
*****************************/
|
||||
/* [ Qwerty & Number ] Common */
|
||||
.xkp_ui_qwerty a,
|
||||
.xkp_ui_qwerty span {display:block; position:relative; text-align:center; height: 40px;}
|
||||
.xkp_ui_qwerty em {width:21px;height:29px;background:url(/images/xkeypad/sp_xkp_white.png) no-repeat;}
|
||||
.xkp_ui_qwerty a { padding-top: 5px; }
|
||||
.xkp_ui_qwerty span { background:none;line-height:30px;vertical-align:middle; padding-top: 3px;}
|
||||
.xkp_ui_qwerty span img{vertical-align:middle; width: 18px; height: 18px;}
|
||||
/* [ Number ] */
|
||||
.xkp_ui_qwerty.xkp_ui_number a,
|
||||
.xkp_ui_qwerty.xkp_ui_number span { height: 50px; }
|
||||
.xkp_ui_qwerty.xkp_ui_number a { padding-top:10px; }
|
||||
.xkp_ui_qwerty.xkp_ui_number span { padding-top: 7px; }
|
||||
/* [ Qwerty & Number ] Refresh Key & Enter Key */
|
||||
.xkp_ui_qwerty.ko .xkp_m106,
|
||||
.xkp_ui_qwerty.ko .xkp_m109,
|
||||
.xkp_ui_qwerty.ko2 .xkp_m106,
|
||||
.xkp_ui_qwerty.ko2 .xkp_m109{width:44px}
|
||||
/* [ Qwerty & Number ] Space Key */
|
||||
.xkp_ui_qwerty a .xkp_m108{width:33px}
|
||||
|
||||
/****************************************************
|
||||
스프라이트 이미지 좌표 설정 ( 키가 눌리지 않았을 때 )
|
||||
*****************************************************/
|
||||
/* [ Qwerty ] 0 ~ 9 */
|
||||
.xkp_ui_qwerty a .xkp_m0{background-position:-207px 0}
|
||||
.xkp_ui_qwerty a .xkp_m1{background-position:0 0}
|
||||
.xkp_ui_qwerty a .xkp_m2{background-position:-23px 0}
|
||||
.xkp_ui_qwerty a .xkp_m3{background-position:-46px 0}
|
||||
.xkp_ui_qwerty a .xkp_m4{background-position:-69px 0}
|
||||
.xkp_ui_qwerty a .xkp_m5{background-position:-92px 0}
|
||||
.xkp_ui_qwerty a .xkp_m6{background-position:-115px 0}
|
||||
.xkp_ui_qwerty a .xkp_m7{background-position:-138px 0}
|
||||
.xkp_ui_qwerty a .xkp_m8{background-position:-161px 0}
|
||||
.xkp_ui_qwerty a .xkp_m9{background-position:-184px 0}
|
||||
/* [ Number ] 0 ~ 9 */
|
||||
.xkp_ui_qwerty a .xkp_m94{background-position:-253px -310px}
|
||||
.xkp_ui_qwerty a .xkp_m95{background-position:-23px -310px}
|
||||
.xkp_ui_qwerty a .xkp_m96{background-position:-46px -310px}
|
||||
.xkp_ui_qwerty a .xkp_m97{background-position:-69px -310px}
|
||||
.xkp_ui_qwerty a .xkp_m98{background-position:-92px -310px}
|
||||
.xkp_ui_qwerty a .xkp_m99{background-position:-115px -310px}
|
||||
.xkp_ui_qwerty a .xkp_m100{background-position:-138px -310px}
|
||||
.xkp_ui_qwerty a .xkp_m101{background-position:-161px -310px}
|
||||
.xkp_ui_qwerty a .xkp_m102{background-position:-184px -310px}
|
||||
.xkp_ui_qwerty a .xkp_m103{background-position:-207px -310px}
|
||||
/* [ Qwerty ] q(ㅂ) ~ p(ㅔ) */
|
||||
.xkp_ui_qwerty a .xkp_m10{background-position:0 -31px}
|
||||
.xkp_ui_qwerty a .xkp_m11{background-position:-23px -31px}
|
||||
.xkp_ui_qwerty a .xkp_m12{background-position:-46px -31px}
|
||||
.xkp_ui_qwerty a .xkp_m13{background-position:-69px -31px}
|
||||
.xkp_ui_qwerty a .xkp_m14{background-position:-92px -31px}
|
||||
.xkp_ui_qwerty a .xkp_m15{background-position:-115px -31px}
|
||||
.xkp_ui_qwerty a .xkp_m16{background-position:-138px -31px}
|
||||
.xkp_ui_qwerty a .xkp_m17{background-position:-161px -31px}
|
||||
.xkp_ui_qwerty a .xkp_m18{background-position:-184px -31px}
|
||||
.xkp_ui_qwerty a .xkp_m19{background-position:-207px -31px}
|
||||
/* [ Qwerty ] a(ㅁ) ~ l(ㅣ) */
|
||||
.xkp_ui_qwerty a .xkp_m20{background-position:0 -62px}
|
||||
.xkp_ui_qwerty a .xkp_m21{background-position:-23px -62px}
|
||||
.xkp_ui_qwerty a .xkp_m22{background-position:-46px -62px}
|
||||
.xkp_ui_qwerty a .xkp_m23{background-position:-69px -62px}
|
||||
.xkp_ui_qwerty a .xkp_m24{background-position:-92px -62px}
|
||||
.xkp_ui_qwerty a .xkp_m25{background-position:-115px -62px}
|
||||
.xkp_ui_qwerty a .xkp_m26{background-position:-138px -62px}
|
||||
.xkp_ui_qwerty a .xkp_m27{background-position:-161px -62px}
|
||||
.xkp_ui_qwerty a .xkp_m28{background-position:-184px -62px}
|
||||
/* [ Qwerty ] z(ㅋ) ~ m(ㅡ) */
|
||||
.xkp_ui_qwerty a .xkp_m29{background-position:-207px -62px}
|
||||
.xkp_ui_qwerty a .xkp_m30{background-position:0 -93px}
|
||||
.xkp_ui_qwerty a .xkp_m31{background-position:-23px -93px}
|
||||
.xkp_ui_qwerty a .xkp_m32{background-position:-46px -93px}
|
||||
.xkp_ui_qwerty a .xkp_m33{background-position:-69px -93px}
|
||||
.xkp_ui_qwerty a .xkp_m34{background-position:-92px -93px}
|
||||
.xkp_ui_qwerty a .xkp_m35{background-position:-115px -93px}
|
||||
/* [ Qwerty ] Capslock Key */
|
||||
.xkp_ui_qwerty a .xkp_m104{background-position:-138px -93px;letter-spacing:-8px}
|
||||
/* [ Qwerty & Number ] Backspace Key */
|
||||
.xkp_ui_qwerty a .xkp_m105{background-position:-161px -93px;letter-spacing:-9px}
|
||||
/* [ Qwerty & Number ] Refresh Key ( XKConfigMobile.functionKeyButtonStyle === 'symbol' ) */
|
||||
.xkp_ui_qwerty a .xkp_m106{background-position:-184px -93px}
|
||||
/* [ Qwerty ] Refresh Key ( XKConfigMobile.functionKeyButtonStyle === 'text' ) */
|
||||
.xkp_ui_qwerty.ko a .xkp_m106{background-position:0 -341px}
|
||||
/* [ Number ] Refresh Key ( XKConfigMobile.functionKeyButtonStyle === 'text' ) */
|
||||
.xkp_ui_qwerty.ko2 a .xkp_m106{background-position:-92px -341px}
|
||||
/* [ Qwerty ] Symbol On/Off Key */
|
||||
.xkp_ui_qwerty a .xkp_m107{background-position:-207px -93px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m107_1{background-position:-191px -342px;letter-spacing:-12px;}
|
||||
/* [ Qwerty & Number ] Space Key */
|
||||
.xkp_ui_qwerty a .xkp_m108{background-position:0 -124px}
|
||||
/* [ Qwerty & Number ] Enter Key ( XKConfigMobile.functionKeyButtonStyle === 'symbol' ) */
|
||||
.xkp_ui_qwerty a .xkp_m109{background-position:-46px -124px}
|
||||
/* [ Qwerty ] Enter Key ( XKConfigMobile.functionKeyButtonStyle === 'text' ) */
|
||||
.xkp_ui_qwerty.ko a .xkp_m109{background-position:-46px -341px}
|
||||
/* [ Number ] Enter Key ( XKConfigMobile.functionKeyButtonStyle === 'text' ) */
|
||||
.xkp_ui_qwerty.ko2 a .xkp_m109{background-position:-138px -341px}
|
||||
/* [ Qwerty (Capslock On) ] Q(ㅃ) ~ P(ㅔ) */
|
||||
.xkp_ui_qwerty a .xkp_m36{background-position:-69px -124px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m37{background-position:-92px -124px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m38{background-position:-115px -124px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m39{background-position:-138px -124px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m40{background-position:-161px -124px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m41{background-position:-184px -124px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m42{background-position:-207px -124px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m43{background-position:0 -155px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m44{background-position:-23px -155px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m45{background-position:-46px -155px;letter-spacing:-12px}
|
||||
/* [ Qwerty (Capslock On) ] A(ㅁ) ~ L(ㅣ) */
|
||||
.xkp_ui_qwerty a .xkp_m46{background-position:-69px -155px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m47{background-position:-92px -155px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m48{background-position:-115px -155px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m49{background-position:-138px -155px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m50{background-position:-161px -155px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m51{background-position:-184px -155px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m52{background-position:-207px -155px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m53{background-position:0 -186px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m54{background-position:-23px -186px;letter-spacing:-12px}
|
||||
/* [ Qwerty (Capslock On) ] Z(ㅋ) ~ M(ㅡ) */
|
||||
.xkp_ui_qwerty a .xkp_m55{background-position:-46px -186px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m56{background-position:-69px -186px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m57{background-position:-92px -186px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m58{background-position:-115px -186px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m59{background-position:-138px -186px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m60{background-position:-161px -186px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m61{background-position:-184px -186px;letter-spacing:-12px}
|
||||
/* [ Qwerty (Symbol On) ] "!" ~ ")" */
|
||||
.xkp_ui_qwerty a .xkp_m62{background-position:-207px -186px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m63{background-position:0 -217px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m64{background-position:-23px -217px}
|
||||
.xkp_ui_qwerty a .xkp_m65{background-position:-46px -217px}
|
||||
.xkp_ui_qwerty a .xkp_m66{background-position:-69px -217px}
|
||||
.xkp_ui_qwerty a .xkp_m67{background-position:-92px -217px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m68{background-position:-115px -217px}
|
||||
.xkp_ui_qwerty a .xkp_m69{background-position:-138px -217px}
|
||||
.xkp_ui_qwerty a .xkp_m70{background-position:-161px -217px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m71{background-position:-184px -217px;letter-spacing:-13px}
|
||||
/* [ Qwerty (Symbol On) ] "[" ~ ";" */
|
||||
.xkp_ui_qwerty a .xkp_m72{background-position:-207px -217px;letter-spacing:-13px}
|
||||
.xkp_ui_qwerty a .xkp_m73{background-position:0 -248px;letter-spacing:-13px}
|
||||
.xkp_ui_qwerty a .xkp_m74{background-position:-23px -248px;letter-spacing:-13px}
|
||||
.xkp_ui_qwerty a .xkp_m75{background-position:-46px -248px;letter-spacing:-13px}
|
||||
.xkp_ui_qwerty a .xkp_m76{background-position:-69px -248px;letter-spacing:-13px}
|
||||
.xkp_ui_qwerty a .xkp_m77{background-position:-92px -248px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m78{background-position:-115px -248px}
|
||||
.xkp_ui_qwerty a .xkp_m79{background-position:-138px -248px}
|
||||
/* [ Qwerty (Symbol On) ] ":" ~ "=" */
|
||||
.xkp_ui_qwerty a .xkp_m80{background-position:-161px -248px}
|
||||
.xkp_ui_qwerty a .xkp_m81{background-position:-184px -248px;letter-spacing:-13px}
|
||||
.xkp_ui_qwerty a .xkp_m82{background-position:-207px -248px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty a .xkp_m83{background-position:0 -279px}
|
||||
.xkp_ui_qwerty a .xkp_m84{background-position:-23px -279px;letter-spacing:-10px}
|
||||
.xkp_ui_qwerty a .xkp_m85{background-position:-46px -279px;letter-spacing:-10px}
|
||||
.xkp_ui_qwerty a .xkp_m86{background-position:-69px -279px}
|
||||
.xkp_ui_qwerty a .xkp_m87{background-position:-92px -279px}
|
||||
/* [ Qwerty (Symbol On) ] "\" ~ "~" */
|
||||
.xkp_ui_qwerty a .xkp_m88{background-position:-115px -279px}
|
||||
.xkp_ui_qwerty a .xkp_m89{background-position:-138px -279px}
|
||||
.xkp_ui_qwerty a .xkp_m90{background-position:-161px -279px}
|
||||
.xkp_ui_qwerty a .xkp_m91{background-position:-184px -279px;letter-spacing:-10px}
|
||||
.xkp_ui_qwerty a .xkp_m92{background-position:-207px -279px;letter-spacing:-13px}
|
||||
.xkp_ui_qwerty a .xkp_m93{background-position:0 -310px;letter-spacing:-13px}
|
||||
|
||||
/****************************************************
|
||||
스프라이트 이미지 좌표 설정 ( 키가 눌렸을 때 )
|
||||
*****************************************************/
|
||||
/* [ Qwerty ] 0 ~ 9 */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m0{background-position:-437px 0}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m1{background-position:-230px 0}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m2{background-position:-253px 0}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m3{background-position:-276px 0}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m4{background-position:-299px 0}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m5{background-position:-322px 0}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m6{background-position:-345px 0}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m7{background-position:-368px 0}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m8{background-position:-391px 0}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m9{background-position:-414px 0}
|
||||
/* [ Qwerty ] q(ㅂ) ~ p(ㅔ) */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m10{background-position:-230px -31px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m11{background-position:-253px -31px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m12{background-position:-276px -31px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m13{background-position:-299px -31px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m14{background-position:-322px -31px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m15{background-position:-345px -31px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m16{background-position:-368px -31px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m17{background-position:-391px -31px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m18{background-position:-414px -31px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m19{background-position:-437px -31px}
|
||||
/* [ Qwerty ] a(ㅁ) ~ l(ㅣ) */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m20{background-position:-230px -62px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m21{background-position:-253px -62px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m22{background-position:-276px -62px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m23{background-position:-299px -62px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m24{background-position:-322px -62px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m25{background-position:-345px -62px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m26{background-position:-368px -62px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m27{background-position:-391px -62px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m28{background-position:-414px -62px}
|
||||
/* [ Qwerty ] z(ㅋ) ~ m(ㅡ) */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m29{background-position:-437px -62px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m30{background-position:-230px -93px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m31{background-position:-253px -93px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m32{background-position:-276px -93px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m33{background-position:-299px -93px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m34{background-position:-322px -93px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m35{background-position:-345px -93px}
|
||||
/* [ Qwerty ] Capslock Key */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m104{background-position:-368px -93px}
|
||||
/* [ Qwerty & Number ] Backspace Key */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m105{background-position:-391px -93px}
|
||||
/* [ Qwerty & Number ] Refresh Key ( XKConfigMobile.functionKeyButtonStyle === 'symbol' ) */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m106{background-position:-414px -93px}
|
||||
/* [ Qwerty ] Refresh Key ( XKConfigMobile.functionKeyButtonStyle === 'text' ) */
|
||||
.xkp_ui_qwerty.has_press_effect.ko a:active .xkp_m106{background-position:-276px -310px}
|
||||
/* [ Number ] Refresh Key ( XKConfigMobile.functionKeyButtonStyle === 'text' ) */
|
||||
.xkp_ui_qwerty.has_press_effect.ko2 a:active .xkp_m106{background-position:-368px -310px}
|
||||
/* [ Qwerty ] Symbol On/Off Key */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m107{background-position:-437px -93px}
|
||||
/* [ Qwerty & Number ] Space Key */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m108{background-position:-230px -124px}
|
||||
/* [ Qwerty & Number ] Enter Key ( XKConfigMobile.functionKeyButtonStyle === 'symbol' ) */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m109{background-position:-276px -124px}
|
||||
/* [ Qwerty ] Enter Key ( XKConfigMobile.functionKeyButtonStyle === 'text' ) */
|
||||
.xkp_ui_qwerty.has_press_effect.ko a:active .xkp_m109{background-position:-322px -310px}
|
||||
/* [ Number ] Enter Key ( XKConfigMobile.functionKeyButtonStyle === 'text' ) */
|
||||
.xkp_ui_qwertyhas_press_effect.ko2 a:active .xkp_m109{background-position:-414px -310px}
|
||||
/* [ Qwerty (Capslock On) ] Q(ㅃ) ~ P(ㅔ) */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m36{background-position:-299px -124px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m37{background-position:-322px -124px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m38{background-position:-345px -124px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m39{background-position:-368px -124px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m40{background-position:-391px -124px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m41{background-position:-414px -124px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m42{background-position:-437px -124px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m43{background-position:-230px -155px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m44{background-position:-253px -155px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m45{background-position:-276px -155px}
|
||||
/* [ Qwerty (Capslock On) ] A(ㅁ) ~ L(ㅣ) */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m46{background-position:-299px -155px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m47{background-position:-322px -155px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m48{background-position:-345px -155px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m49{background-position:-368px -155px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m50{background-position:-391px -155px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m51{background-position:-414px -155px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m52{background-position:-437px -155px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m53{background-position:-230px -186px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m54{background-position:-253px -186px}
|
||||
/* [ Qwerty (Capslock On) ] Z(ㅋ) ~ M(ㅡ) */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m55{background-position:-276px -186px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m56{background-position:-299px -186px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m57{background-position:-322px -186px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m58{background-position:-345px -186px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m59{background-position:-368px -186px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m60{background-position:-391px -186px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m61{background-position:-414px -186px}
|
||||
/* [ Qwerty (Symbol On) ] "!" ~ ")" */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m62{background-position:-437px -186px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m63{background-position:-230px -217px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m64{background-position:-253px -217px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m65{background-position:-276px -217px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m66{background-position:-299px -217px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m67{background-position:-322px -217px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m68{background-position:-345px -217px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m69{background-position:-368px -217px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m70{background-position:-391px -217px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m71{background-position:-414px -217px}
|
||||
/* [ Qwerty (Symbol On) ] "[" ~ ";" */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m72{background-position:-437px -217px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m73{background-position:-230px -248px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m74{background-position:-253px -248px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m75{background-position:-276px -248px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m76{background-position:-299px -248px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m77{background-position:-322px -248px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m78{background-position:-345px -248px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m79{background-position:-368px -248px}
|
||||
/* [ Qwerty (Symbol On) ] ":" ~ "=" */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m80{background-position:-391px -248px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m81{background-position:-414px -248px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m82{background-position:-437px -248px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m83{background-position:-230px -279px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m84{background-position:-253px -279px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m85{background-position:-276px -279px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m86{background-position:-299px -279px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m87{background-position:-322px -279px}
|
||||
/* [ Qwerty (Symbol On) ] "\" ~ "~" */
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m88{background-position:-345px -279px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m89{background-position:-368px -279px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m90{background-position:-391px -279px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m91{background-position:-414px -279px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m92{background-position:-437px -279px}
|
||||
.xkp_ui_qwerty.has_press_effect a:active .xkp_m93{background-position:-230px -310px}
|
||||
|
||||
@media (min-width: 840px) and (min-height: 630px) {
|
||||
/* [ Qwerty & Number ] Common */
|
||||
.xkp_ui_qwerty.auto_resize a { padding-top: 7px; }
|
||||
.xkp_ui_qwerty.auto_resize a,
|
||||
.xkp_ui_qwerty.auto_resize span { height: 60px; }
|
||||
.xkp_ui_qwerty.auto_resize em{width:31.5px;height:43.5px;background:url(/images/xkeypad/sp_xkp_white_big.png) no-repeat;}
|
||||
.xkp_ui_qwerty.auto_resize span { padding-top: 11px;}
|
||||
.xkp_ui_qwerty.auto_resize span img{width: 24.5px; height: 24.5px;}
|
||||
/* [ Number ] */
|
||||
.xkp_ui_qwerty.xkp_ui_number.auto_resize a,
|
||||
.xkp_ui_qwerty.xkp_ui_number.auto_resize span { height: 70px; }
|
||||
.xkp_ui_qwerty.xkp_ui_number.auto_resize a { padding-top:12px; }
|
||||
.xkp_ui_qwerty.xkp_ui_number.auto_resize span { padding-top: 18px; }
|
||||
/* [ Qwerty & Number ] Refresh Key & Enter Key */
|
||||
.xkp_ui_qwerty.ko.auto_resize .xkp_m106,
|
||||
.xkp_ui_qwerty.ko.auto_resize .xkp_m109,
|
||||
.xkp_ui_qwerty.ko2.auto_resize .xkp_m106,
|
||||
.xkp_ui_qwerty.ko2.auto_resize .xkp_m109{width:66px;}
|
||||
/* [ Qwerty & Number ] Space Key */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m108{width:50px}
|
||||
|
||||
/****************************************************
|
||||
스프라이트 이미지 좌표 설정 ( 키가 눌리지 않았을 때 )
|
||||
*****************************************************/
|
||||
/* [ Qwerty ] 0 ~ 9 */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m0{background-position:-310.5px 0}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m1{background-position:0 0}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m2{background-position:-34.5px 0}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m3{background-position: -69px 0}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m4{background-position:-103.5px 0}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m5{background-position:-138px 0}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m6{background-position:-172.5px 0}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m7{background-position:-207px 0}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m8{background-position:-241.5px 0}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m9{background-position:-276px 0}
|
||||
/* [ Number ] 0 ~ 9 */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m94{background-position:-379.5px -465px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m95{background-position:-34.5px -465px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m96{background-position:-69px -465px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m97{background-position:-103.5px -465px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m98{background-position:-138px -465px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m99{background-position:-172.5px -465px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m100{background-position:-207px -465px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m101{background-position:-241.5px -465px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m102{background-position:-276px -465px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m103{background-position:-310.5px -465px}
|
||||
/* [ Qwerty ] q(ㅂ) ~ p(ㅔ) */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m10{background-position:0 -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m11{background-position:-34.5px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m12{background-position:-69px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m13{background-position:-103.5px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m14{background-position:-138px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m15{background-position:-172.5px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m16{background-position:-207px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m17{background-position:-241.5px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m18{background-position:-276px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m19{background-position:-310.5px -46.5px}
|
||||
/* [ Qwerty ] a(ㅁ) ~ l(ㅣ) */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m20{background-position:0 -93px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m21{background-position:-34.5px -93px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m22{background-position:-69px -93px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m23{background-position:-103.5px -93px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m24{background-position:-138px -93px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m25{background-position:-172.5px -93px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m26{background-position:-207px -93px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m27{background-position:-241.5px -93px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m28{background-position:-276px -93px}
|
||||
/* [ Qwerty ] z(ㅋ) ~ m(ㅡ) */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m29{background-position:-310.5px -93px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m30{background-position:0 -139.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m31{background-position:-34.5px -139.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m32{background-position:-69px -139.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m33{background-position:-103.5px -139.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m34{background-position:-138px -139.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m35{background-position:-172.5px -139.5px}
|
||||
/* [ Qwerty ] Capslock Key */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m104{background-position:-207px -139.5px;letter-spacing:-8px}
|
||||
/* [ Qwerty & Number ] Backspace Key */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m105{background-position:-241.5px -139.5px;letter-spacing:-9px}
|
||||
/* [ Qwerty & Number ] Refresh Key ( XKConfigMobile.functionKeyButtonStyle === 'symbol' ) */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m106{background-position:-276px -139.5px}
|
||||
/* [ Qwerty ] Refresh Key ( XKConfigMobile.functionKeyButtonStyle === 'text' ) */
|
||||
.xkp_ui_qwerty.ko.auto_resize a .xkp_m106{background-position:0 -511.5px}
|
||||
/* [ Number ] Refresh Key ( XKConfigMobile.functionKeyButtonStyle === 'text' ) */
|
||||
.xkp_ui_qwerty.ko2.auto_resize a .xkp_m106{background-position:-138px -511.5px}
|
||||
/* [ Qwerty ] Symbol On/Off Key */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m107{background-position:-310.5px -139.5px; letter-spacing:-12px}
|
||||
/* [ Qwerty & Number ] Space Key */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m108{background-position:0 -186px}
|
||||
/* [ Qwerty & Number ] Enter Key ( XKConfigMobile.functionKeyButtonStyle === 'symbol' ) */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m109{background-position:-69px -186px}
|
||||
/* [ Qwerty ] Enter Key ( XKConfigMobile.functionKeyButtonStyle === 'text' ) */
|
||||
.xkp_ui_qwerty.ko.auto_resize a .xkp_m109{background-position:-69px -511.5px}
|
||||
/* [ Number ] Enter Key ( XKConfigMobile.functionKeyButtonStyle === 'text' ) */
|
||||
.xkp_ui_qwerty.ko2.auto_resize a .xkp_m109{background-position:-207px -511.5px}
|
||||
/* [ Qwerty (Capslock On) ] Q(ㅃ) ~ P(ㅔ) */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m36{background-position:-103.5px -186px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m37{background-position:-138px -186px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m38{background-position:-172.5px -186px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m39{background-position:-207px -186px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m40{background-position:-241.5px -186px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m41{background-position:-276px -186px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m42{background-position:-310.5px -186px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m43{background-position: 0 -232.5px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m44{background-position:-34.5px -232.5px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m45{background-position:-69px -232.5px;letter-spacing:-12px}
|
||||
/* [ Qwerty (Capslock On) ] A(ㅁ) ~ L(ㅣ) */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m46{background-position:-103.5px -232.5px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m47{background-position:-138px -232.5px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m48{background-position:-172.5px -232.5px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m49{background-position:-207px -232.5px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m50{background-position:-241.5px -232.5px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m51{background-position:-276px -232.5px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m52{background-position:-310.5px -232.5px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m53{background-position:0 -279px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m54{background-position:-34.5px -279px;letter-spacing:-12px}
|
||||
/* [ Qwerty (Capslock On) ] Z(ㅋ) ~ M(ㅡ) */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m55{background-position:-69px -279px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m56{background-position:-103.5px -279px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m57{background-position:-138px -279px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m58{background-position:-172.5px -279px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m59{background-position:-207px -279px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m60{background-position:-241.5px -279px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m61{background-position:-276px -279px;letter-spacing:-12px}
|
||||
/* [ Qwerty (Symbol On) ] "!" ~ ")" */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m62{background-position:-310.5px -279px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m63{background-position:0 -325.5px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m64{background-position:-34.5px -325.5px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m65{background-position:-69px -325.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m66{background-position:-103.5px -325.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m67{background-position:-138px -325.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m68{background-position:-172.5px -325.5px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m69{background-position:-207px -325.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m70{background-position:-241.5px -325.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m71{background-position:-276px -325.5px;letter-spacing:-12px}
|
||||
/* [ Qwerty (Symbol On) ] "[" ~ ";" */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m72{background-position:-310.5px -325.5px;letter-spacing:-13px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m73{background-position:0 -372px;letter-spacing:-13px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m74{background-position:-34.5px -372px;letter-spacing:-13px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m75{background-position:-69px -372px;letter-spacing:-13px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m76{background-position:-103.5px -372px;letter-spacing:-13px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m77{background-position:-138px -372px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m78{background-position:-172.5px -372px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m79{background-position:-207px -372px}
|
||||
/* [ Qwerty (Symbol On) ] ":" ~ "=" */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m80{background-position:-241.5px -372px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m81{background-position:-276px -372px;letter-spacing:-13px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m82{background-position:-310.5px -372px;letter-spacing:-12px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m83{background-position:0 -418.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m84{background-position:-34.5px -418.5px;letter-spacing:-10px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m85{background-position:-69px -418.5px;letter-spacing:-10px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m86{background-position:-103.5px -418.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m87{background-position:-138px -418.5px}
|
||||
/* [ Qwerty (Symbol On) ] "\" ~ "~" */
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m88{background-position:-172.5px -418.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m89{background-position:-207px -418.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m90{background-position:-241.5px -418.5px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m91{background-position:-276px -418.5px;letter-spacing:-10px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m92{background-position:-310.5px -418.5px;letter-spacing:-13px}
|
||||
.xkp_ui_qwerty.auto_resize a .xkp_m93{background-position:0 -465px;letter-spacing:-13px}
|
||||
|
||||
/****************************************************
|
||||
스프라이트 이미지 좌표 설정 ( 키가 눌렸을 때 )
|
||||
*****************************************************/
|
||||
/* [ Qwerty ] 0 ~ 9 */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m0{background-position:-655.5px 0}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m1{background-position:-345px 0}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m2{background-position:-379.5px 0}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m3{background-position:-414px 0}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m4{background-position:-448.5px 0}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m5{background-position:-483px 0}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m6{background-position:-517.5px 0}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m7{background-position:-552px 0}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m8{background-position:-586.5px 0}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m9{background-position:-621px 0}
|
||||
/* [ Qwerty ] q(ㅂ) ~ p(ㅔ) */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m10{background-position:-345px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m11{background-position:-379.5px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m12{background-position:-414px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m13{background-position:-448.5px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m14{background-position:-483px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m15{background-position:-517.5px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m16{background-position:-552px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m17{background-position:-586.5px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m18{background-position:-621px -46.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m19{background-position:-655.5px -46.5px}
|
||||
/* [ Qwerty ] a(ㅁ) ~ l(ㅣ) */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m20{background-position:-345px -93px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m21{background-position:-379.5px -93px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m22{background-position:-414px -93px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m23{background-position:-448.5px -93px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m24{background-position:-483px -93px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m25{background-position:-517.5px -93px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m26{background-position:-552px -93px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m27{background-position:-586.5px -93px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m28{background-position:-621px -93px}
|
||||
/* [ Qwerty ] z(ㅋ) ~ m(ㅡ) */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m29{background-position:-655.5px -93px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m30{background-position:-345px -139.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m31{background-position:-379.5px -139.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m32{background-position:-414px -139.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m33{background-position:-448.5px -139.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m34{background-position:-483px -139.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m35{background-position:-517.5px -139.5px}
|
||||
/* [ Qwerty ] Capslock Key */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m104{background-position:-552px -139.5px}
|
||||
/* [ Qwerty & Number ] Backspace Key */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m105{background-position:-587.5px -139.5px}
|
||||
/* [ Qwerty & Number ] Refresh Key ( XKConfigMobile.functionKeyButtonStyle === 'symbol' ) */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m106{background-position:-621px -139.5px}
|
||||
/* [ Qwerty ] Refresh Key ( XKConfigMobile.functionKeyButtonStyle === 'text' ) */
|
||||
.xkp_ui_qwerty.ko.auto_resize.has_press_effect a:active .xkp_m106{background-position:-414px -465px}
|
||||
/* [ Number ] Refresh Key ( XKConfigMobile.functionKeyButtonStyle === 'text' ) */
|
||||
.xkp_ui_qwerty.ko2.auto_resize.has_press_effect a:active .xkp_m106{background-position:-552px -465px}
|
||||
/* [ Qwerty ] Symbol On/Off Key */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m107{background-position:-655.5px -139.5px}
|
||||
/* [ Qwerty & Number ] Space Key */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m108{background-position:-345px -186px}
|
||||
/* [ Qwerty & Number ] Enter Key ( XKConfigMobile.functionKeyButtonStyle === 'symbol' ) */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m109{background-position:-414px -186px}
|
||||
/* [ Qwerty ] Enter Key ( XKConfigMobile.functionKeyButtonStyle === 'text' ) */
|
||||
.xkp_ui_qwerty.ko.auto_resize.has_press_effect a:active .xkp_m109{background-position:-483px -465px}
|
||||
/* [ Number ] Enter Key ( XKConfigMobile.functionKeyButtonStyle === 'text' ) */
|
||||
.xkp_ui_qwerty.ko2.auto_resize.has_press_effect a:active .xkp_m109{background-position:-621px -465px}
|
||||
/* [ Qwerty (Capslock On) ] Q(ㅃ) ~ P(ㅔ) */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m36{background-position:-448.5px -186px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m37{background-position:-483px -186px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m38{background-position:-517.5px -186px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m39{background-position:-552px -186px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m40{background-position:-586.5px -186px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m41{background-position:-621px -186px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m42{background-position:-655.5px -186px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m43{background-position:-345px -232.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m44{background-position:-379.5px -232.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m45{background-position:-414px -232.5px}
|
||||
/* [ Qwerty (Capslock On) ] A(ㅁ) ~ L(ㅣ) */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m46{background-position:-448.5px -232.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m47{background-position:-483px -232.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m48{background-position:-517.5px -232.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m49{background-position:-552px -232.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m50{background-position:-586.5px -232.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m51{background-position:-621px -232.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m52{background-position:-655.5px -232.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m53{background-position:-345px -279px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m54{background-position:-379.5px -279px}
|
||||
/* [ Qwerty (Capslock On) ] Z(ㅋ) ~ M(ㅡ) */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m55{background-position:-414px -279px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m56{background-position:-448.5px -279px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m57{background-position:-483px -279px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m58{background-position:-517.5px -279px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m59{background-position:-552px -279px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m60{background-position:-586.5px -279px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m61{background-position:-621px -279px}
|
||||
/* [ Qwerty (Symbol On) ] "!" ~ ")" */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m62{background-position:-655.5px -279px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m63{background-position:-345px -325.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m64{background-position:-379.5px -325.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m65{background-position:-414px -325.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m66{background-position:-448.5px -325.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m67{background-position:-483px -325.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m68{background-position:-517.5px -325.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m69{background-position:-552px -325.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m70{background-position:-586.5px -325.5px}
|
||||
/* [ Qwerty (Symbol On) ] "[" ~ ";" */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m71{background-position:-621px -325.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m72{background-position:-655.5px -325.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m73{background-position:-345px -372px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m74{background-position:-379.5px -372px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m75{background-position:-414px -372px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m76{background-position:-448.5px -372px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m77{background-position:-483px -372px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m78{background-position:-517.5px -372px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m79{background-position:-552px -372px}
|
||||
/* [ Qwerty (Symbol On) ] ":" ~ "=" */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m80{background-position:-586.5px -372px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m81{background-position:-621px -372px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m82{background-position:-655.5px -372px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m83{background-position:-345px -418.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m84{background-position:-379.5px -418.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m85{background-position:-414px -418.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m86{background-position:-448.5px -418.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m87{background-position:-483px -418.5px}
|
||||
/* [ Qwerty (Symbol On) ] "\" ~ "~" */
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m88{background-position:-517.5px -418.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m89{background-position:-552px -418.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m90{background-position:-586.5px -418.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m91{background-position:-621px -418.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m92{background-position:-655.5px -418.5px}
|
||||
.xkp_ui_qwerty.auto_resize.has_press_effect a:active .xkp_m93{background-position:-345px -465px}
|
||||
}
|
||||
|
||||
/* keypad내에 위치할 경우 */
|
||||
/*
|
||||
.xkalert_overlay{width:100%;height:100%;position:fixed;top:0;left:0;background-image:url(/images/xkeypad/overlay.png);z-index:10;}
|
||||
.xkalert_frame{width:100%;position:absolute;top:15%;}
|
||||
*/
|
||||
|
||||
/* 기존 alert 위치할 경우 */
|
||||
.xkalert_overlay{width:100%;height:100%;position:fixed;top:0;left:0;background-image:url(/images/xkeypad/overlay.png);z-index:10;}
|
||||
.xkalert_frame{width:100%;position:absolute;top:35%;}
|
||||
|
||||
.xkalert_box{font-family:"Helvetica Neue", Helvetica, Arial, sans-serif;font-size:14px;width:90%;height:115px;margin:auto;text-align:center;border:1px solid gray;background-color:white;border-radius:3px;padding-top:15px;z-index:10;}
|
||||
.xkalert_box hr{border:0;height:0;border-top:1px solid rgba(0, 0, 0, 0.1);border-bottom:1px solid rgba(255, 255, 255, 0.3);}
|
||||
.xkalert_box .content{text-align:center;vertical-align:middle;height:57px;}
|
||||
.xkalert_box .bottom{position:absolute;width:90%;bottom:10px;margin:auto;}
|
||||
.xkalert_box .btn {display:inline-block;text-decoration:none;font-weight:bold;line-height:240%;color:rgb(102,102,102);text-align:center;background-color:white;width:100px;height:30px;border-color:rgb(180,180,180);border-width:1px;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;border-style:solid;padding-top:3px;}
|
||||
1
src/shared/ui/assets/js/rsa_crypto.js
Executable file
35
src/shared/ui/assets/js/xkeypad.js
Executable file
64
src/shared/ui/assets/js/xkeypad_config.js
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* XKeypad Configuration
|
||||
* 가상 키패드 설정 파일
|
||||
*/
|
||||
|
||||
// XKeypad Mobile Configuration
|
||||
window.XKConfigMobile = {
|
||||
// 최대 입력 길이
|
||||
maxInputSize: 16,
|
||||
|
||||
// 기본 키패드 타입
|
||||
defaultKeyType: 'qwertysmart',
|
||||
|
||||
// 기본 뷰 타입
|
||||
defaultViewType: 'normal',
|
||||
|
||||
// 기본 숫자 키패드 행 개수
|
||||
defaultNumberKeyRowCount: 3,
|
||||
|
||||
// 키패드 닫힘 지연 시간 (ms)
|
||||
defaultCloseDelay: 300,
|
||||
|
||||
// E2E 통신 기본 활성화
|
||||
defaultIsE2E: true,
|
||||
|
||||
// 모바일 전용 모드 기본값
|
||||
defaultOnlyMobile: false,
|
||||
|
||||
// 키 음영 효과 기본값
|
||||
defaultHasPressEffect: true,
|
||||
|
||||
// 자동 키 크기 조절 기본값
|
||||
defaultAutoKeyResize: false,
|
||||
|
||||
// 키패드 기본 너비 (%)
|
||||
defaultWidth: 100,
|
||||
|
||||
logoImgPath: '/images/xkeypad/blank_key.png',
|
||||
|
||||
// 서버 URL (E2E 통신용)
|
||||
serverUrl: '',
|
||||
|
||||
// 공개키 (RSA)
|
||||
rsaPublicKey: {
|
||||
modulus: "C4F7B39E2E93DB19C016C7A0C1C05B028A1D57CB9B91E13F5B7353F8FB5AC6CE6BE31ABEB8E8F7AD18B90C08F4EBC011A6A8FCE614EA879ED5B96296B969CE92923BC9BAD6FD87F00E08F529F93010EA77E40937BDAC1C866E79ACE2F2822A3ECD982F90532D5301CF90D9BF89E953A0593AB6C5F31E99B690DD582FB85F85A9",
|
||||
exponent: "10001"
|
||||
},
|
||||
|
||||
// 디버그 모드
|
||||
debug: false,
|
||||
|
||||
// 로그 레벨
|
||||
logLevel: 'error' // 'none', 'error', 'warn', 'info', 'debug'
|
||||
};
|
||||
|
||||
// 전역 설정 함수
|
||||
window.setXKConfig = function(config) {
|
||||
window.XKConfigMobile = Object.assign(window.XKConfigMobile || {}, config);
|
||||
};
|
||||
|
||||
// 초기화 확인 플래그
|
||||
window.XKConfigLoaded = true;
|
||||
|
||||
console.log('XKConfigMobile initialized');
|
||||
656
src/utils/xkeypad.ts
Normal file
@@ -0,0 +1,656 @@
|
||||
// XKModule 및 관련 타입 정의
|
||||
declare global {
|
||||
interface Window {
|
||||
XKModule: any;
|
||||
XKConfigMobile: {
|
||||
maxInputSize: number;
|
||||
rsaPublicKey?: {
|
||||
n: string;
|
||||
e: string;
|
||||
};
|
||||
};
|
||||
RSASetPublic: (N: string, E: string) => void;
|
||||
RSAEncrypt: (text: string) => string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface XKSessionInfo {
|
||||
sessionId: string;
|
||||
input: string;
|
||||
secToken: string;
|
||||
}
|
||||
|
||||
export interface XKModuleInstance {
|
||||
initialize: (options: any) => number;
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
isOpen: () => boolean;
|
||||
clear: () => void;
|
||||
destroy: () => void;
|
||||
get_input: () => string;
|
||||
get_sessionInfo: () => XKSessionInfo;
|
||||
setRSAPublicKey: (n: string, e: string) => void;
|
||||
}
|
||||
|
||||
export type KeyType = 'qwertysmart' | 'number';
|
||||
export type ViewType = 'half' | 'normal';
|
||||
export type NumberKeyRowCount = 2 | 3 | 4;
|
||||
|
||||
export interface XKeypadOptions {
|
||||
keyType?: KeyType;
|
||||
viewType?: ViewType;
|
||||
numberKeyRowCount?: NumberKeyRowCount;
|
||||
maxInputSize?: number;
|
||||
width?: number;
|
||||
position?: { top?: number | null; left?: number | null };
|
||||
closeDelay?: number;
|
||||
autoKeyResize?: boolean;
|
||||
isE2E?: boolean;
|
||||
onlyMobile?: boolean;
|
||||
hasPressEffect?: boolean;
|
||||
useModal?: boolean;
|
||||
useOverlay?: boolean; // 오버레이 사용 여부
|
||||
onInputChange?: (newLength: number) => void;
|
||||
onKeypadClose?: () => void;
|
||||
}
|
||||
|
||||
export interface XKeypadResult {
|
||||
type: 'E2E' | 'Plain';
|
||||
sessionId?: string;
|
||||
encryptedInput?: string;
|
||||
secToken?: string;
|
||||
plainText?: string;
|
||||
rsaEncrypted?: string;
|
||||
}
|
||||
|
||||
export interface RSAConfig {
|
||||
modulus: string;
|
||||
exponent: string;
|
||||
}
|
||||
|
||||
// 기본 RSA 키 설정
|
||||
const DEFAULT_RSA_CONFIG: RSAConfig = {
|
||||
modulus: "C4F7B39E2E93DB19C016C7A0C1C05B028A1D57CB9B91E13F5B7353F8FB5AC6CE6BE31ABEB8E8F7AD18B90C08F4EBC011A6A8FCE614EA879ED5B96296B969CE92923BC9BAD6FD87F00E08F529F93010EA77E40937BDAC1C866E79ACE2F2822A3ECD982F90532D5301CF90D9BF89E953A0593AB6C5F31E99B690DD582FB85F85A9",
|
||||
exponent: "10001"
|
||||
};
|
||||
|
||||
export class XKeypadManager {
|
||||
private static instance: XKeypadManager;
|
||||
private scriptsLoaded: boolean = false;
|
||||
private loadingPromise: Promise<void> | null = null;
|
||||
private xkModules: Map<string, XKModuleInstance> = new Map();
|
||||
private rsaConfig: RSAConfig;
|
||||
|
||||
private constructor(rsaConfig?: RSAConfig) {
|
||||
this.rsaConfig = rsaConfig || DEFAULT_RSA_CONFIG;
|
||||
}
|
||||
|
||||
public static getInstance(rsaConfig?: RSAConfig): XKeypadManager {
|
||||
if (!XKeypadManager.instance) {
|
||||
XKeypadManager.instance = new XKeypadManager(rsaConfig);
|
||||
}
|
||||
return XKeypadManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* XKeypad 스크립트를 로드합니다.
|
||||
*/
|
||||
public async loadScripts(): Promise<void> {
|
||||
// 이미 로드 중이거나 로드됨
|
||||
if (this.loadingPromise) {
|
||||
return this.loadingPromise;
|
||||
}
|
||||
|
||||
if (this.scriptsLoaded && window.XKModule) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.loadingPromise = this.loadScriptsInternal();
|
||||
await this.loadingPromise;
|
||||
this.scriptsLoaded = true;
|
||||
}
|
||||
|
||||
private async loadScriptsInternal(): Promise<void> {
|
||||
try {
|
||||
// Check if scripts are already loaded
|
||||
if (window.XKModule) {
|
||||
this.scriptsLoaded = true;
|
||||
// Set RSA keys after scripts are loaded
|
||||
if (this.rsaConfig && window.RSASetPublic) {
|
||||
window.RSASetPublic(this.rsaConfig.modulus, this.rsaConfig.exponent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Load scripts in order
|
||||
const scripts = [
|
||||
'/src/shared/ui/assets/js/xkeypad_config.js',
|
||||
'/src/shared/ui/assets/js/rsa_crypto.js',
|
||||
'/src/shared/ui/assets/js/xkeypad.js'
|
||||
];
|
||||
|
||||
for (const src of scripts) {
|
||||
await this.loadScript(src);
|
||||
}
|
||||
|
||||
// Add CSS files
|
||||
this.loadCSS('/src/shared/ui/assets/css/xkeypad-modal.css', 'xkeypad-modal-css');
|
||||
this.loadCSS('/src/shared/ui/assets/css/xkeypad.css', 'xkeypad-css');
|
||||
|
||||
// Set RSA keys after scripts are loaded
|
||||
if (this.rsaConfig && window.RSASetPublic) {
|
||||
window.RSASetPublic(this.rsaConfig.modulus, this.rsaConfig.exponent);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load XKeypad scripts:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private loadScript(src: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.charset = 'utf-8';
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
private loadCSS(href: string, id: string): void {
|
||||
if (!document.getElementById(id)) {
|
||||
const link = document.createElement('link');
|
||||
link.id = id;
|
||||
link.rel = 'stylesheet';
|
||||
link.href = href;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 새로운 XKModule 인스턴스를 생성합니다.
|
||||
*/
|
||||
public createModule(id: string): XKModuleInstance | null {
|
||||
if (!this.scriptsLoaded || !window.XKModule) {
|
||||
console.error('XKeypad scripts not loaded');
|
||||
return null;
|
||||
}
|
||||
|
||||
const module = new window.XKModule();
|
||||
this.xkModules.set(id, module);
|
||||
return module;
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 XKModule 인스턴스를 가져옵니다.
|
||||
*/
|
||||
public getModule(id: string): XKModuleInstance | null {
|
||||
return this.xkModules.get(id) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* XKModule 인스턴스를 제거합니다.
|
||||
*/
|
||||
public removeModule(id: string): void {
|
||||
const module = this.xkModules.get(id);
|
||||
if (module) {
|
||||
if (module.isOpen()) {
|
||||
module.close();
|
||||
}
|
||||
this.xkModules.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 키패드를 닫습니다.
|
||||
*/
|
||||
public closeAllKeypads(): void {
|
||||
this.xkModules.forEach(module => {
|
||||
if (module && module.isOpen()) {
|
||||
module.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA 공개키를 설정합니다.
|
||||
*/
|
||||
public setRSAPublicKey(modulus: string, exponent: string): void {
|
||||
this.rsaConfig = { modulus, exponent };
|
||||
|
||||
// RSASetPublic이 있는지 확인하고 설정
|
||||
if (window.RSASetPublic && typeof window.RSASetPublic === 'function') {
|
||||
try {
|
||||
window.RSASetPublic(modulus, exponent);
|
||||
} catch (error) {
|
||||
console.error('Failed to set RSA public key:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update all existing modules
|
||||
this.xkModules.forEach(module => {
|
||||
if (module && typeof module.setRSAPublicKey === 'function') {
|
||||
module.setRSAPublicKey(modulus, exponent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA로 텍스트를 암호화합니다.
|
||||
*/
|
||||
public encryptRSA(text: string): string | null {
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// RSAEncrypt 함수가 있는지 확인
|
||||
if (!window.RSAEncrypt || typeof window.RSAEncrypt !== 'function') {
|
||||
console.error('RSA encryption not available');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// RSA 키가 설정되어 있는지 확인
|
||||
if (this.rsaConfig && window.RSASetPublic) {
|
||||
window.RSASetPublic(this.rsaConfig.modulus, this.rsaConfig.exponent);
|
||||
}
|
||||
|
||||
return window.RSAEncrypt(text);
|
||||
} catch (error) {
|
||||
console.error('RSA encryption failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* XKeypad 인스턴스를 관리하는 클래스
|
||||
*/
|
||||
export class XKeypad {
|
||||
private manager: XKeypadManager;
|
||||
private module: XKModuleInstance | null = null;
|
||||
private moduleId: string;
|
||||
private options: XKeypadOptions;
|
||||
private containerName: string;
|
||||
private inputElement: HTMLInputElement | null = null;
|
||||
private overlayElement: HTMLDivElement | null = null;
|
||||
|
||||
constructor(
|
||||
moduleId: string,
|
||||
options: XKeypadOptions = {},
|
||||
rsaConfig?: RSAConfig
|
||||
) {
|
||||
this.manager = XKeypadManager.getInstance(rsaConfig);
|
||||
this.moduleId = moduleId;
|
||||
this.options = {
|
||||
keyType: 'qwertysmart',
|
||||
viewType: 'half',
|
||||
numberKeyRowCount: 3,
|
||||
maxInputSize: 50,
|
||||
width: 100,
|
||||
closeDelay: 300,
|
||||
autoKeyResize: false,
|
||||
isE2E: false,
|
||||
onlyMobile: false,
|
||||
hasPressEffect: true,
|
||||
useModal: false,
|
||||
useOverlay: true, // 기본값: 오버레이 사용
|
||||
...options
|
||||
};
|
||||
this.containerName = this.options.useModal
|
||||
? `xk-modal-${moduleId}`
|
||||
: `xk-pad-${moduleId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 키패드를 초기화합니다.
|
||||
*/
|
||||
public async initialize(inputElement: HTMLInputElement): Promise<number> {
|
||||
// 스크립트 로드
|
||||
await this.manager.loadScripts();
|
||||
|
||||
// 입력 요소 저장
|
||||
this.inputElement = inputElement;
|
||||
|
||||
// 모듈 생성
|
||||
this.module = this.manager.createModule(this.moduleId);
|
||||
if (!this.module) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// RSA 키 설정 (E2E가 아닌 경우)
|
||||
if (!this.options.isE2E) {
|
||||
const rsaConfig = (this.manager as any).rsaConfig;
|
||||
if (rsaConfig && typeof this.module.setRSAPublicKey === 'function') {
|
||||
this.module.setRSAPublicKey(rsaConfig.modulus, rsaConfig.exponent);
|
||||
}
|
||||
}
|
||||
|
||||
// 모달 컨테이너 생성 (필요시)
|
||||
if (this.options.useModal) {
|
||||
this.createModalContainer();
|
||||
}
|
||||
|
||||
// 키패드 초기화
|
||||
const result = this.module.initialize({
|
||||
name: this.containerName,
|
||||
editBox: inputElement,
|
||||
keyType: this.options.keyType,
|
||||
maxInputSize: this.options.maxInputSize,
|
||||
width: this.options.width,
|
||||
position: {
|
||||
top: this.options.useModal ? 0 : (this.options.position?.top ?? null),
|
||||
left: this.options.position?.left ?? null
|
||||
},
|
||||
viewType: this.options.useModal ? 'normal' : this.options.viewType,
|
||||
numberKeyRowCount: this.options.numberKeyRowCount,
|
||||
closeDelay: this.options.closeDelay,
|
||||
autoKeyResize: this.options.autoKeyResize,
|
||||
isE2E: this.options.isE2E,
|
||||
onlyMobile: this.options.onlyMobile,
|
||||
hasPressEffect: this.options.hasPressEffect,
|
||||
onInputChange: this.options.onInputChange,
|
||||
onKeypadClose: () => {
|
||||
if (this.options.useModal) {
|
||||
this.hideModal();
|
||||
}
|
||||
if (this.options.useOverlay && !this.options.useModal) {
|
||||
this.hideOverlay();
|
||||
}
|
||||
if (this.options.onKeypadClose) {
|
||||
this.options.onKeypadClose();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (result === 0) {
|
||||
if (this.options.useModal) {
|
||||
this.showModal();
|
||||
} else if (this.options.useOverlay) {
|
||||
this.showOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 키패드를 엽니다.
|
||||
*/
|
||||
public open(): void {
|
||||
if (this.module && !this.module.isOpen()) {
|
||||
this.module.open();
|
||||
if (this.options.useModal) {
|
||||
this.showModal();
|
||||
} else if (this.options.useOverlay) {
|
||||
this.showOverlay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 키패드를 닫습니다.
|
||||
*/
|
||||
public close(): void {
|
||||
if (this.module && this.module.isOpen()) {
|
||||
this.module.close();
|
||||
if (this.options.useModal) {
|
||||
this.hideModal();
|
||||
} else if (this.options.useOverlay) {
|
||||
this.hideOverlay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 키패드가 열려있는지 확인합니다.
|
||||
*/
|
||||
public isOpen(): boolean {
|
||||
return this.module ? this.module.isOpen() : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 입력을 초기화합니다.
|
||||
*/
|
||||
public clear(): void {
|
||||
if (this.module) {
|
||||
this.module.clear();
|
||||
}
|
||||
if (this.inputElement) {
|
||||
this.inputElement.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 키패드를 파괴합니다.
|
||||
*/
|
||||
public destroy(): void {
|
||||
if (this.module) {
|
||||
if (this.module.isOpen()) {
|
||||
this.module.close();
|
||||
}
|
||||
this.module.destroy();
|
||||
}
|
||||
this.manager.removeModule(this.moduleId);
|
||||
|
||||
// Remove modal container if exists
|
||||
if (this.options.useModal) {
|
||||
const wrapper = document.getElementById(`${this.containerName}-wrapper`);
|
||||
if (wrapper) {
|
||||
wrapper.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove overlay if exists
|
||||
if (this.options.useOverlay && !this.options.useModal) {
|
||||
this.hideOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 입력 값을 가져옵니다.
|
||||
*/
|
||||
public getValue(): XKeypadResult | null {
|
||||
if (!this.module) return null;
|
||||
|
||||
const result: XKeypadResult = {} as XKeypadResult;
|
||||
|
||||
if (this.options.isE2E) {
|
||||
const sessionInfo = this.module.get_sessionInfo();
|
||||
result.type = 'E2E';
|
||||
result.sessionId = sessionInfo.sessionId;
|
||||
result.encryptedInput = sessionInfo.input;
|
||||
result.secToken = sessionInfo.secToken;
|
||||
} else {
|
||||
const plainText = this.module.get_input();
|
||||
result.type = 'Plain';
|
||||
result.plainText = plainText;
|
||||
|
||||
// RSA encryption if available
|
||||
if (plainText) {
|
||||
const encrypted = this.manager.encryptRSA(plainText);
|
||||
if (encrypted) {
|
||||
result.rsaEncrypted = encrypted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 평문 입력 값을 가져옵니다.
|
||||
*/
|
||||
public getPlainText(): string {
|
||||
if (!this.module) return '';
|
||||
return this.module.get_input();
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 정보를 가져옵니다. (E2E 모드에서만 유효)
|
||||
*/
|
||||
public getSessionInfo(): XKSessionInfo | null {
|
||||
if (!this.module || !this.options.isE2E) return null;
|
||||
return this.module.get_sessionInfo();
|
||||
}
|
||||
|
||||
// Modal helper methods
|
||||
private createModalContainer(): void {
|
||||
const name = this.containerName;
|
||||
|
||||
// Remove existing container if any
|
||||
const existing = document.getElementById(`${name}-wrapper`);
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
}
|
||||
|
||||
// Create modal structure
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.id = `${name}-wrapper`;
|
||||
wrapper.className = 'xkeypad-modal-wrapper';
|
||||
wrapper.style.display = 'none';
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'xkeypad-modal-overlay';
|
||||
overlay.onclick = () => this.close();
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'xkeypad-modal-container';
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.id = name;
|
||||
content.className = 'xkeypad-modal-content';
|
||||
|
||||
container.appendChild(content);
|
||||
wrapper.appendChild(overlay);
|
||||
wrapper.appendChild(container);
|
||||
document.body.appendChild(wrapper);
|
||||
}
|
||||
|
||||
private showModal(): void {
|
||||
const wrapper = document.getElementById(`${this.containerName}-wrapper`);
|
||||
if (wrapper) {
|
||||
wrapper.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
wrapper.classList.add('show');
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
private hideModal(): void {
|
||||
const wrapper = document.getElementById(`${this.containerName}-wrapper`);
|
||||
if (wrapper) {
|
||||
wrapper.classList.remove('show');
|
||||
setTimeout(() => {
|
||||
wrapper.style.display = 'none';
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay methods for non-modal mode
|
||||
private showOverlay(): void {
|
||||
// Remove existing overlay if any
|
||||
this.hideOverlay();
|
||||
|
||||
// Create overlay
|
||||
this.overlayElement = document.createElement('div');
|
||||
this.overlayElement.id = `xkeypad-overlay-${this.moduleId}`;
|
||||
this.overlayElement.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9998;
|
||||
transition: opacity 0.3s ease;
|
||||
opacity: 0;
|
||||
`;
|
||||
|
||||
// Add click handler to close keypad
|
||||
this.overlayElement.onclick = () => this.close();
|
||||
|
||||
// Append to body
|
||||
document.body.appendChild(this.overlayElement);
|
||||
|
||||
// Trigger transition
|
||||
setTimeout(() => {
|
||||
if (this.overlayElement) {
|
||||
this.overlayElement.style.opacity = '1';
|
||||
}
|
||||
}, 10);
|
||||
|
||||
// Adjust keypad z-index to be above overlay
|
||||
const keypadContainer = document.getElementById(this.containerName);
|
||||
if (keypadContainer) {
|
||||
keypadContainer.style.zIndex = '9999';
|
||||
}
|
||||
}
|
||||
|
||||
private hideOverlay(): void {
|
||||
if (this.overlayElement) {
|
||||
this.overlayElement.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
if (this.overlayElement && this.overlayElement.parentNode) {
|
||||
this.overlayElement.parentNode.removeChild(this.overlayElement);
|
||||
this.overlayElement = null;
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 간편 사용을 위한 헬퍼 함수들
|
||||
*/
|
||||
|
||||
/**
|
||||
* 비밀번호 입력용 키패드를 생성합니다.
|
||||
*/
|
||||
export function createPasswordKeypad(
|
||||
inputElement: HTMLInputElement,
|
||||
options?: Partial<XKeypadOptions>,
|
||||
rsaConfig?: RSAConfig
|
||||
): XKeypad {
|
||||
return new XKeypad('password', {
|
||||
keyType: 'qwertysmart',
|
||||
maxInputSize: 16,
|
||||
...options
|
||||
}, rsaConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* PIN 입력용 키패드를 생성합니다.
|
||||
*/
|
||||
export function createPinKeypad(
|
||||
inputElement: HTMLInputElement,
|
||||
options?: Partial<XKeypadOptions>,
|
||||
rsaConfig?: RSAConfig
|
||||
): XKeypad {
|
||||
return new XKeypad('pin', {
|
||||
keyType: 'number',
|
||||
maxInputSize: 6,
|
||||
...options
|
||||
}, rsaConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드번호 입력용 키패드를 생성합니다.
|
||||
*/
|
||||
export function createCardKeypad(
|
||||
inputElement: HTMLInputElement,
|
||||
options?: Partial<XKeypadOptions>,
|
||||
rsaConfig?: RSAConfig
|
||||
): XKeypad {
|
||||
return new XKeypad('card', {
|
||||
keyType: 'number',
|
||||
maxInputSize: 16,
|
||||
numberKeyRowCount: 4,
|
||||
...options
|
||||
}, rsaConfig);
|
||||
}
|
||||
|
||||
// Default export
|
||||
export default XKeypad;
|
||||