// 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 | null = null; private xkModules: Map = 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 { // 이미 로드 중이거나 로드됨 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 { 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 { 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 { // 스크립트 로드 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, rsaConfig?: RSAConfig ): XKeypad { return new XKeypad('password', { keyType: 'qwertysmart', maxInputSize: 16, ...options }, rsaConfig); } /** * PIN 입력용 키패드를 생성합니다. */ export function createPinKeypad( inputElement: HTMLInputElement, options?: Partial, rsaConfig?: RSAConfig ): XKeypad { return new XKeypad('pin', { keyType: 'number', maxInputSize: 6, ...options }, rsaConfig); } /** * 카드번호 입력용 키패드를 생성합니다. */ export function createCardKeypad( inputElement: HTMLInputElement, options?: Partial, rsaConfig?: RSAConfig ): XKeypad { return new XKeypad('card', { keyType: 'number', maxInputSize: 16, numberKeyRowCount: 4, ...options }, rsaConfig); } // Default export export default XKeypad;