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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/images/xkeypad/button.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

BIN
public/images/xkeypad/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

BIN
public/images/xkeypad/overlay.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

BIN
public/images/xkeypad/xkcur.cur Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/images/xkeypad/xkcur.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

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

View File

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

View 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;
}

View 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;

View 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%;
}

View 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;

View 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%;
}
}

View File

@@ -84,6 +84,8 @@ const AdditionalServicePages = lazyLoad('/src/pages/additional-service/addition
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 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>

View File

@@ -162,7 +162,8 @@ export const ROUTE_NAMES = {
base: '/alarm/*',
list: 'list',
},
xkeypad: '/xkeypad',
xkeypadSample: '/xkeypad-sample',
};
export type RouteNamesType = typeof ROUTE_NAMES;

View 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;
}
}

View 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;}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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
View 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;