@fuzionx/helpdesk 0.1.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpdesk.es.js","sources":["../src/core/HelpdeskConfig.js","../src/core/HelpdeskEvents.js","../src/core/HelpdeskClient.js","../src/vue/HelpdeskWidget.vue","../src/vue/plugin.js","../src/vanilla/HelpdeskEmbed.js"],"sourcesContent":["/**\n * HelpdeskConfig — 옵션 기본값 및 검증\n *\n * HelpdeskClient / Vue Plugin / Vanilla Embed 에서 공통 사용.\n * 사용자 옵션을 기본값과 병합하고 필수값을 검증한다.\n *\n * @module core/HelpdeskConfig\n */\n\n/** @type {Object} 기본 옵션 */\nconst DEFAULTS = {\n /**\n * WebSocket URL (선택)\n * 빈 문자열이면 현재 도메인 기준 자동 구성.\n * 외부 사이트에서는 'wss://example.com/ws' 형태로 지정.\n */\n wsUrl: '',\n /** WS 네임스페이스 경로 */\n wsNamespace: '/support-chat',\n\n // ── ASP/WASM 설정 ──\n /**\n * ASP 암/복호화 활성화 여부\n * true: FuzionXSocket 내부에서 자동 ASP 암/복호화\n * false: 평문 통신 (개발/테스트용)\n */\n aspEnabled: true,\n /**\n * ASP masterSecret (동일 도메인 SPA에서 provide로 전달)\n * 빈 문자열이면 connect() 시 자동 추출 시도.\n */\n masterSecret: '',\n /**\n * WASM 파일 URL\n * 동일 도메인에서는 '/wasm/fuzionx_client_wasm.js' 자동 사용.\n * 외부 사이트에서는 CDN URL 지정 필요.\n */\n wasmUrl: '/wasm/fuzionx_client_wasm.js',\n\n /** 템플릿 이름 (default / minimal / bubble) */\n template: 'default',\n /** 테마 (light / dark / auto) */\n theme: 'auto',\n /** FAB 위치 (bottom-right / bottom-left) */\n position: 'bottom-right',\n /** 위젯 타이틀 */\n title: '고객상담',\n /** 입장안내 환영 메시지 */\n welcomeMessage: '안녕하세요! 무엇을 도와드릴까요?',\n /** 상담 가능 시간 안내 */\n operatingHours: '09:00~18:00',\n /** 주말/공휴일 안내 */\n offDayNotice: '주말/공휴일 휴무',\n /** WS 자동 연결 여부 */\n autoConnect: false,\n /** 인증 토큰 (외부 사이트 전용 JWT) */\n authToken: '',\n /** 팝업 모드 (true: window.open, false: 인라인 위젯) */\n popupMode: false,\n /** 다국어 메시지 오버라이드 */\n i18n: null,\n /** 위젯 z-index */\n zIndex: 9990,\n};\n\n/**\n * 옵션 병합 + 검증\n *\n * @param {Object} userOpts - 사용자 옵션\n * @returns {Object} 검증된 최종 옵션\n */\nexport function resolveConfig(userOpts = {}) {\n const config = { ...DEFAULTS, ...userOpts };\n\n // theme 검증\n if (!['light', 'dark', 'auto'].includes(config.theme)) {\n console.warn(`[fuzionx-helpdesk] 알 수 없는 theme: \"${config.theme}\", 기본값 \"auto\" 사용`);\n config.theme = 'auto';\n }\n\n // position 검증\n if (!['bottom-right', 'bottom-left'].includes(config.position)) {\n console.warn(`[fuzionx-helpdesk] 알 수 없는 position: \"${config.position}\", 기본값 \"bottom-right\" 사용`);\n config.position = 'bottom-right';\n }\n\n return config;\n}\n\nexport { DEFAULTS };\n","/**\n * HelpdeskEvents — 이벤트 상수 정의\n *\n * WebSocket 메시지 type 필드에 사용되는 이벤트명.\n * 서버(SupportChatHandler.js)와 동일한 값 유지 필수.\n *\n * @module core/HelpdeskEvents\n */\n\n/** 회원 → 서버 이벤트 */\nexport const CLIENT_EVENTS = {\n /** 상담사 온라인 여부 확인 (입장 버튼 클릭 시) */\n CHECK_AGENTS: 'checkAgents',\n /** 상담 요청 → 세션 생성 */\n REQUEST_CHAT: 'requestChat',\n /** 메시지 전송 */\n MESSAGE: 'message',\n /** 회원이 상담 종료 */\n CLOSE_CHAT: 'closeChat',\n /** 입력 중 상태 전송 */\n TYPING: 'typing',\n /** 과거 세션 복구 요청 */\n RESUME_SESSION: 'resumeSession',\n};\n\n/** 서버 → 회원 이벤트 */\nexport const SERVER_EVENTS = {\n /** 상담사 온라인 상태 응답 */\n AGENT_STATUS: 'agentStatus',\n /** 대기열 등록 완료 */\n CHAT_QUEUED: 'chatQueued',\n /** 상담원 입장 (첫 번째) */\n AGENT_JOINED: 'agentJoined',\n /** 메시지 수신 (FuzionXSocket 'message' 예약 이벤트 충돌 방지) */\n MESSAGE: 'newMessage',\n /** Primary agent 퇴장 */\n AGENT_LEFT: 'agentLeft',\n /** 상담 종료 */\n CHAT_CLOSED: 'chatClosed',\n /** 에러 */\n ERROR: 'error',\n /** 상대방의 입력 중 상태 수신 */\n TYPING: 'typing',\n};\n\n/** UI 내부 이벤트 (컴포넌트 간 통신용) */\nexport const UI_EVENTS = {\n /** 위젯 열기 */\n OPEN: 'helpdesk:open',\n /** 위젯 닫기 */\n CLOSE: 'helpdesk:close',\n /** 화면 전환: welcome → waiting → chat → closed */\n VIEW_CHANGE: 'helpdesk:viewChange',\n /** 연결 상태 변경 */\n CONNECTION_CHANGE: 'helpdesk:connectionChange',\n};\n","/**\n * HelpdeskClient — 고객 상담 WebSocket 코어 클라이언트\n *\n * WASM FuzionXSocket을 사용하여 ASP 암/복호화를 내부 처리.\n * useWebSocket.js 패턴을 참조하여 동일한 방식으로 WS 연결.\n *\n * 연결 흐름:\n * 1. window._aspReady 대기 (WASM 로드)\n * 2. FuzionXSocket 클래스 로드 (없으면 동적 import)\n * 3. new FuzionXSocket(url, masterSecret, aspEnabled) 생성\n * 4. socket.on('event', handler) 로 이벤트 수신\n * 5. socket.send(type, data) 로 메시지 전송\n *\n * @module core/HelpdeskClient\n */\nimport { resolveConfig } from './HelpdeskConfig.js';\nimport { CLIENT_EVENTS, SERVER_EVENTS, UI_EVENTS } from './HelpdeskEvents.js';\n\n/**\n * @typedef {'welcome'|'checking'|'unavailable'|'waiting'|'chatting'|'closed'} ViewState\n */\n\nexport default class HelpdeskClient {\n /**\n * @param {Object} opts - 사용자 옵션 (HelpdeskConfig 참조)\n */\n constructor(opts) {\n /** @type {Object} 검증된 옵션 */\n this.config = resolveConfig(opts);\n\n /** @type {Object|null} FuzionXSocket 인스턴스 (WASM) */\n this._ws = null;\n\n /** @type {boolean} WS 연결 상태 */\n this.connected = false;\n\n /** @type {string|null} 현재 세션 키 */\n this.sessionKey = null;\n\n /** @type {ViewState} 현재 화면 상태 */\n this.viewState = 'welcome';\n\n /** @type {Array<Object>} 메시지 목록 */\n this.messages = [];\n\n /** @type {number} 대기열 순번 */\n this.queuePosition = 0;\n\n /** @type {string} 상담원 이름 */\n this.agentName = null;\n\n /** @type {boolean} 상담사 타이핑 상태 */\n this.isAgentTyping = false;\n\n /** @type {number|null} 타이핑 상태 타이머 */\n this._typingTimer = null;\n\n /** @type {Object} 상담사 상태 (checkAgents 응답) */\n this.agentStatus = { available: false, count: 0 };\n\n /** @type {Array<Object>} 상담 이력 */\n this.history = [];\n\n /** @type {Map<string, Set<Function>>} 이벤트 리스너 */\n this._listeners = new Map();\n\n /** @type {string} masterSecret (ASP용) */\n this._masterSecret = this.config.masterSecret || '';\n\n /** @type {boolean} ASP 암/복호화 활성 여부 */\n this._aspEnabled = this.config.aspEnabled;\n }\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // EventEmitter\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n /**\n * 이벤트 리스너 등록\n *\n * @param {string} event - 이벤트명\n * @param {Function} handler - 핸들러 함수\n * @returns {HelpdeskClient} 체이닝용\n */\n on(event, handler) {\n if (!this._listeners.has(event)) {\n this._listeners.set(event, new Set());\n }\n this._listeners.get(event).add(handler);\n return this;\n }\n\n /**\n * 이벤트 리스너 제거\n *\n * @param {string} event - 이벤트명\n * @param {Function} handler - 핸들러 함수\n */\n off(event, handler) {\n this._listeners.get(event)?.delete(handler);\n }\n\n /**\n * 이벤트 발행\n *\n * @param {string} event - 이벤트명\n * @param {*} data - 페이로드\n * @private\n */\n _emit(event, data) {\n this._listeners.get(event)?.forEach(fn => {\n try { fn(data); } catch (e) { console.error(`[Helpdesk] 이벤트 에러 (${event}):`, e); }\n });\n }\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // WebSocket 연결 관리\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n /**\n * WS 연결 (FuzionXSocket 기반)\n *\n * useWebSocket.js connect() 패턴 동일:\n * 1. _aspReady 대기\n * 2. FuzionXSocket 클래스 로드\n * 3. new FuzionXSocket(url, masterSecret, aspEnabled)\n * 4. 이벤트 바인딩 + connect()\n *\n * @returns {Promise<void>}\n */\n async connect() {\n if (this._ws && this.connected) {\n return; // 이미 연결됨\n }\n\n // ── 1. WASM 로드 대기 (main.js에서 이미 초기화한 경우) ──\n if (window._aspReady) {\n await window._aspReady;\n }\n\n // ── 2. FuzionXSocket 클래스 로드 (없으면 동적 import) ──\n if (!window.FuzionXSocket) {\n try {\n const _v = Date.now(); // 캐시 방지\n const wasmJsUrl = this.config.wasmUrl || '/wasm/fuzionx_client_wasm.js';\n const wasmBgUrl = wasmJsUrl.replace('.js', '_bg.wasm');\n const mod = await import(/* @vite-ignore */ `${wasmJsUrl}?v=${_v}`);\n await mod.default({ module_or_path: `${wasmBgUrl}?v=${_v}` });\n window.FuzionXSocket = mod.FuzionXSocket;\n } catch (e) {\n console.error('[Helpdesk] WASM 로드 실패:', e);\n return;\n }\n }\n\n // ── 3. WS URL 구성 (/ws 접두사 + 네임스페이스) ──\n const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';\n const wsUrl = this.config.wsUrl || `${wsProto}//${location.host}/ws`;\n const url = `${wsUrl}${this.config.wsNamespace}`;\n\n // ── 4. FuzionXSocket 생성 ──\n // masterSecret: main.js에서 provide로 전달받은 값\n // aspEnabled: ASP 암/복호화 활성화 여부\n this._ws = new window.FuzionXSocket(url, this._masterSecret, this._aspEnabled);\n\n // ── 5. 이벤트 바인딩 (type별 on() 등록) ──\n this._ws.on('connect', () => {\n this.connected = true;\n this._emit(UI_EVENTS.CONNECTION_CHANGE, { connected: true });\n console.log('[Helpdesk] WS 연결됨');\n });\n\n this._ws.on('disconnect', (info) => {\n this.connected = false;\n this._emit(UI_EVENTS.CONNECTION_CHANGE, { connected: false });\n console.log('[Helpdesk] WS 연결 해제:', info);\n // 재연결은 WASM FuzionXSocket이 내부적으로 처리\n });\n\n this._ws.on('error', () => {\n console.error('[Helpdesk] WS 에러');\n });\n\n // welcome 이벤트 (서버 입장 확인)\n this._ws.on('welcome', (data) => {\n console.log('[Helpdesk] welcome:', data);\n });\n\n // 서버 메시지 수신부: FuzionXSocket은 WASM 내부에서 파싱된 JSON을 'message' 이벤트로 일괄 전달합니다.\n this._ws.on('message', (data) => {\n this._handleMessage(data);\n });\n\n // ── 6. 연결 ──\n this._ws.connect();\n }\n\n /** WS 연결 해제 */\n disconnect() {\n if (this._ws) {\n this._ws.disconnect();\n this._ws = null;\n }\n this.connected = false;\n }\n\n /**\n * WS 메시지 전송 (FuzionXSocket.send 사용)\n *\n * @param {string} type - 이벤트 타입\n * @param {Object} data - 페이로드\n * @private\n */\n _send(type, data = {}) {\n if (this._ws && this._ws.is_connected()) {\n this._ws.send(type, data);\n } else {\n console.warn('[Helpdesk] WS 연결 안됨, 메시지 전송 불가:', type);\n }\n }\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // WS 수신 메시지 처리\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n /**\n * 서버 메시지 핸들러\n *\n * FuzionXSocket의 'message' 이벤트에서 호출됨.\n * ASP 복호화는 FuzionXSocket 내부에서 자동 처리.\n *\n * @param {Object|string} raw - 서버 메시지 (자동 JSON 파싱 or 문자열)\n * @private\n */\n _handleMessage(raw) {\n let msg;\n try {\n msg = typeof raw === 'string' ? JSON.parse(raw) : raw;\n } catch (e) {\n console.error('[Helpdesk] 메시지 파싱 오류:', e);\n return;\n }\n\n const { type, data } = msg;\n\n switch (type) {\n case SERVER_EVENTS.AGENT_STATUS:\n // 상담사 가용 여부 응답\n this.agentStatus = data;\n if (data.available) {\n this._setView('checking');\n } else {\n this._setView('unavailable');\n }\n this._emit(type, data);\n break;\n\n case SERVER_EVENTS.CHAT_QUEUED:\n // 대기열 등록 완료\n this.sessionKey = data.sessionKey;\n this.queuePosition = data.position;\n if (this.sessionKey) {\n localStorage.setItem('fx_support_session', this.sessionKey);\n }\n \n if (data.messages && data.messages.length > 0) {\n this.messages = data.messages.map(m => ({\n userId: m.user_id,\n userName: m.nickname || m.username || (m.role_code ? '상담사' : '나'),\n content: m.content,\n messageType: m.message_type || 'text',\n createdAt: m.created_at,\n isAgent: m.role_code === 'admin' || m.role_code === 'developer'\n }));\n }\n\n this._setView('waiting');\n this._emit(type, data);\n break;\n\n case SERVER_EVENTS.AGENT_JOINED:\n // 상담원 입장 → 채팅 화면 전환\n this.sessionKey = data.sessionKey || this.sessionKey;\n this.agentName = data.nickname || data.username || data.agentName || '상담사';\n this.queuePosition = 0; // 대기 순번 초기화\n if (this.sessionKey) {\n localStorage.setItem('fx_support_session', this.sessionKey);\n }\n this._setView('chatting');\n \n if (data.messages && data.messages.length > 0) {\n this.messages = data.messages.map(m => ({\n userId: m.user_id,\n userName: m.nickname || m.username || (m.role_code ? this.agentName : '나'),\n content: m.content,\n messageType: m.message_type || 'text',\n createdAt: m.created_at,\n isAgent: m.role_code === 'admin' || m.role_code === 'developer'\n }));\n this._addSystemMessage('이전 대화 내역을 불러왔습니다.');\n } else {\n this._addSystemMessage('상담원이 입장했습니다.');\n }\n this._emit(type, data);\n break;\n\n case SERVER_EVENTS.TYPING:\n // 상담사가 입력 중일 때\n this.isAgentTyping = data.isTyping;\n if (this._typingTimer) clearTimeout(this._typingTimer);\n // 일정 시간 후 자동으로 타이핑 상태 제거 (방어탑)\n if (this.isAgentTyping) {\n this._typingTimer = setTimeout(() => {\n this.isAgentTyping = false;\n }, 3000);\n }\n this._emit(type, data);\n break;\n\n case SERVER_EVENTS.MESSAGE:\n // 실시간 메시지 수신 (상담원 메시지)\n this.messages.push({\n userId: data.userId,\n userName: data.userName,\n content: data.content,\n messageType: data.messageType || 'text',\n createdAt: data.createdAt,\n isAgent: true,\n });\n this._emit(type, data);\n break;\n\n case SERVER_EVENTS.AGENT_LEFT:\n // 상담원이 모두 나감 → 입력 차단 (waiting으로 전환)\n this._addSystemMessage('상담원 연결이 해제되었습니다. 잠시 후 다시 연결됩니다.');\n this._setView('waiting');\n this._emit(type, data);\n break;\n\n case SERVER_EVENTS.CHAT_CLOSED:\n // 상담 종료 (Continuous Chat을 위해 로컬 스토리지는 삭제하지 않음)\n if (data && data.messages && data.messages.length > 0) {\n this.messages = data.messages.map(m => ({\n userId: m.user_id,\n userName: m.nickname || m.username || (m.role_code ? '상담사' : '나'),\n content: m.content,\n messageType: m.message_type || 'text',\n createdAt: m.created_at,\n isAgent: m.role_code === 'admin' || m.role_code === 'developer'\n }));\n }\n this._addSystemMessage('상담이 종료되었습니다.');\n this._setView('closed');\n this._emit(type, data);\n break;\n\n case 'resumeFailed':\n // 세션 복구 실패 (종료되었거나 사라진 세션)\n localStorage.removeItem('fx_support_session');\n this.sessionKey = null;\n this._setView('welcome');\n this._emit(type, data);\n break;\n\n case SERVER_EVENTS.ERROR:\n console.error('[Helpdesk] 서버 에러:', data.message);\n this._emit(type, data);\n break;\n\n default:\n this._emit(type, data);\n }\n }\n\n /**\n * 서버 이벤트 직접 핸들러\n *\n * FuzionXSocket이 type별로 직접 dispatch하는 경우 처리.\n * 예: ws.on('agentStatus', data) → _handleServerEvent('agentStatus', data)\n *\n * @param {string} type - 이벤트 타입\n * @param {Object} data - 페이로드\n * @private\n */\n _handleServerEvent(type, data) {\n // { type, data } 형태로 래핑하여 _handleMessage에 위임\n this._handleMessage({ type, data });\n }\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 공개 API (회원 액션)\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n /**\n * 저장소에 과거 연결된 세션 키가 있는지 검사\n * @returns {boolean}\n */\n hasSavedSession() {\n return !!localStorage.getItem('fx_support_session');\n }\n\n /**\n * 이전 세션 정보로 다시 조인 시도\n * 서버에서 성공 시 agentJoined나 chatQueued 반환\n */\n resumeSession() {\n const sessionKey = localStorage.getItem('fx_support_session');\n if (!sessionKey) return;\n this._send(CLIENT_EVENTS.RESUME_SESSION, {\n sessionKey,\n userId: this.config._userId || null,\n });\n }\n\n /**\n * 상담사 가용 여부 확인\n *\n * \"상담 입장하기\" 클릭 시 호출.\n * 결과는 agentStatus 이벤트로 수신.\n */\n checkAgents() {\n this._send(CLIENT_EVENTS.CHECK_AGENTS, {});\n }\n\n /**\n * 상담 요청 (세션 생성)\n *\n * checkAgents → available=true 확인 후 호출.\n *\n * @param {Object} opts\n * @param {number} [opts.userId] - 회원 ID\n * @param {string} [opts.subject] - 상담 주제\n */\n requestChat({ userId = null, subject = null } = {}) {\n if (this._requestChatLock) return;\n this._requestChatLock = true;\n \n this._send(CLIENT_EVENTS.REQUEST_CHAT, {\n userId,\n subject,\n sessionKey: this.sessionKey || localStorage.getItem('fx_support_session'),\n });\n\n // 1초 후 락 해제 (네트워크 오류 재시도 허용)\n setTimeout(() => {\n this._requestChatLock = false;\n }, 1000);\n }\n\n /**\n * 메시지 전송\n *\n * @param {string} content - 메시지 내용\n * @param {Object} [userInfo] - 발신자 정보 { userId, username, nickname }\n */\n sendMessage(content, userInfo = {}) {\n if (!this.sessionKey || !content.trim()) return;\n\n const trimmed = content.trim();\n\n // FuzionXSocket.send(type, data)\n this._send(CLIENT_EVENTS.MESSAGE, {\n sessionKey: this.sessionKey,\n content: trimmed,\n userId: userInfo.userId || null,\n username: userInfo.username || null,\n nickname: userInfo.nickname || null,\n roleCode: 'member',\n });\n\n // 로컬 즉시 반영 (낙관적 업데이트)\n this.messages.push({\n userId: userInfo.userId,\n userName: userInfo.nickname || userInfo.username || '나',\n content: trimmed,\n messageType: 'text',\n createdAt: new Date().toISOString(),\n isAgent: false,\n _isLocal: true,\n });\n\n this._emit(UI_EVENTS.VIEW_CHANGE, { view: this.viewState, messages: this.messages });\n }\n\n /**\n * 타이핑 상태 전송\n * @param {boolean} isTyping - 입력 중 상태\n */\n sendTyping(isTyping) {\n if (!this.sessionKey) return;\n this._send(CLIENT_EVENTS.TYPING, {\n sessionKey: this.sessionKey,\n isTyping,\n });\n }\n\n /**\n * 상담 종료 (회원 측)\n */\n closeChat() {\n if (!this.sessionKey) return;\n this._send(CLIENT_EVENTS.CLOSE_CHAT, { sessionKey: this.sessionKey });\n }\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 내부 헬퍼\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n /**\n * 화면 상태 전환\n *\n * @param {ViewState} view\n * @private\n */\n _setView(view) {\n this.viewState = view;\n this._emit(UI_EVENTS.VIEW_CHANGE, { view, messages: this.messages });\n }\n\n /**\n * 시스템 메시지 추가\n *\n * @param {string} content\n * @private\n */\n _addSystemMessage(content) {\n this.messages.push({\n content,\n messageType: 'system',\n createdAt: new Date().toISOString(),\n });\n }\n\n /**\n * 상태 초기화 (새 상담 시작 전)\n */\n reset() {\n this.sessionKey = null;\n this.viewState = 'welcome';\n this.messages = [];\n this.queuePosition = 0;\n this.agentName = '';\n this.agentStatus = { available: false, count: 0 };\n }\n\n /** 리소스 정리 */\n destroy() {\n this.disconnect();\n this._listeners.clear();\n this.reset();\n }\n}\n","<template>\n <div :class=\"['hd-widget', `hd-pos-${config.position}`, `hd-theme-${effectiveTheme}`]\" :style=\"{ zIndex: config.zIndex }\">\n <!-- FAB (Floating Action Button) -->\n <button v-if=\"!isOpen\" class=\"hd-fab\" @click=\"open\" :title=\"config.title\">\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"/></svg>\n </button>\n\n <!-- 위젯 패널 -->\n <transition name=\"hd-slide\">\n <div v-if=\"isOpen\" class=\"hd-panel\">\n <!-- 패널 헤더 -->\n <div class=\"hd-panel-hd\">\n <span class=\"hd-panel-title\">{{ config.title }}</span>\n <button class=\"hd-panel-close\" @click=\"close\">&times;</button>\n </div>\n\n <!-- ── 환영 화면 (welcome) ── -->\n <div v-if=\"view === 'welcome'\" class=\"hd-view hd-view-welcome\">\n <div class=\"hd-welcome-icon\">💬</div>\n <p class=\"hd-welcome-msg\">{{ config.welcomeMessage }}</p>\n <p class=\"hd-welcome-hours\">\n <i>🕐</i> {{ config.operatingHours }}\n <span v-if=\"config.offDayNotice\"> · {{ config.offDayNotice }}</span>\n </p>\n <button class=\"hd-btn-enter\" @click=\"handleEnter\">상담 입장하기</button>\n </div>\n\n <!-- ── 상담사 확인 중 (checking) ── -->\n <div v-else-if=\"view === 'checking'\" class=\"hd-view hd-view-checking\">\n <div class=\"hd-spinner\"></div>\n <p>상담사를 확인하고 있습니다...</p>\n </div>\n\n <!-- ── 상담사 부재 (unavailable) ── -->\n <div v-else-if=\"view === 'unavailable'\" class=\"hd-view hd-view-unavail\">\n <div class=\"hd-unavail-icon\">😔</div>\n <p class=\"hd-unavail-msg\">현재 상담 가능한 상담사가 없습니다.</p>\n <p class=\"hd-unavail-sub\">운영시간: {{ config.operatingHours }}</p>\n <button class=\"hd-btn-retry\" @click=\"handleEnter\">다시 확인</button>\n <button class=\"hd-btn-back\" @click=\"goWelcome\">돌아가기</button>\n </div>\n\n <!-- ── 대기 중 (waiting) ── -->\n <div v-else-if=\"view === 'waiting'\" class=\"hd-view hd-view-waiting\">\n <div class=\"hd-spinner\"></div>\n <p class=\"hd-wait-msg\">상담사 연결 대기 중...</p>\n <p class=\"hd-wait-pos\" v-if=\"client.queuePosition\">대기 순번: <strong>{{ client.queuePosition }}</strong></p>\n </div>\n\n <!-- ── 채팅 (chatting) ── -->\n <div v-else-if=\"view === 'chatting' || view === 'closed'\" class=\"hd-view hd-view-chat\">\n <div class=\"hd-chat-agent\" v-if=\"client.agentName\">\n 상담원: <strong>{{ client.agentName }}</strong>\n </div>\n\n <!-- 메시지 영역 -->\n <div class=\"hd-chat-msgs\" ref=\"msgBox\">\n <div v-for=\"(m, i) in client.messages\" :key=\"i\" :class=\"['hd-msg', msgCls(m)]\">\n <div v-if=\"m.messageType === 'system'\" class=\"hd-msg-sys\">{{ m.content }}</div>\n <template v-else>\n <div class=\"hd-msg-bubble\">{{ m.content }}</div>\n <div class=\"hd-msg-time\">{{ fmtTime(m.createdAt) }}</div>\n </template>\n </div>\n <!-- 상담사 입력 중 표시 -->\n <div v-if=\"client.isAgentTyping\" class=\"hd-msg hd-msg-agent\">\n <div class=\"hd-msg-bubble hd-typing-indicator\">\n <span>.</span><span>.</span><span>.</span>\n </div>\n </div>\n </div>\n\n <!-- 입력 (chatting일 때만) -->\n <div class=\"hd-chat-input\" v-if=\"view === 'chatting'\">\n <input\n type=\"text\"\n v-model=\"inputText\"\n @input=\"handleTyping\"\n @keyup.enter=\"handleSend\"\n placeholder=\"메시지를 입력하세요...\"\n />\n <button class=\"hd-btn-send\" @click=\"handleSend\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M2 21l21-9L2 3v7l15 2-15 2z\"/></svg>\n </button>\n </div>\n\n <!-- 종료 상태 -->\n <div class=\"hd-chat-ended\" v-if=\"view === 'closed'\">\n <p>상담이 종료되었습니다.</p>\n <button class=\"hd-btn-new\" @click=\"handleNewChat\">새 상담 시작</button>\n </div>\n </div>\n </div>\n </transition>\n </div>\n</template>\n\n<script setup>\n/**\n * HelpdeskWidget.vue — 고객 상담 위젯 메인 컴포넌트\n *\n * FAB → 입장안내 → 상담사확인 → 대기 → 채팅 → 종료\n * HelpdeskClient 코어와 연동.\n */\nimport { ref, computed, watch, nextTick, onMounted, onUnmounted, inject } from 'vue';\nimport { UI_EVENTS, SERVER_EVENTS } from '../core/HelpdeskEvents.js';\n\nconst props = defineProps({\n user: {\n type: Object,\n default: null,\n }\n});\n\n/** HelpdeskClient는 Vue Plugin에서 provide로 주입 */\nconst client = inject('helpdeskClient');\n/** Config도 주입 */\nconst config = inject('helpdeskConfig');\n\n/** 위젯 열림 상태 */\nconst isOpen = ref(false);\n/** 현재 화면 상태 */\nconst view = ref('welcome');\n/** 메시지 입력 텍스트 */\nconst inputText = ref('');\n/** 메시지 스크롤 컨테이너 */\nconst msgBox = ref(null);\n\n/** 테마 자동 감지 */\nconst effectiveTheme = computed(() => {\n if (config.theme !== 'auto') return config.theme;\n // 브라우저 다크모드 감지\n return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n});\n\n// ── 이벤트 리스너 ──\nfunction onViewChange(data) {\n view.value = data.view;\n // 채팅 화면으로 전환 시 자동 스크롤\n if (data.view === 'chatting') {\n nextTick(() => scrollToBottom());\n }\n}\n\nfunction onConnectionChange(data) {\n if (data.connected && client.hasSavedSession()) {\n view.value = 'checking';\n client.resumeSession();\n }\n}\n\n// ── 유저 상태 동기화 ──\nwatch(() => props.user, (u) => {\n if (u && u._id) {\n config._userId = u._id;\n config._username = u.username;\n config._nickname = u.nickname || u.name;\n } else {\n config._userId = null;\n config._username = null;\n config._nickname = null;\n }\n}, { immediate: true });\n\n/** 자동 스크롤 */\nfunction scrollToBottom() {\n if (msgBox.value) {\n msgBox.value.scrollTop = msgBox.value.scrollHeight;\n }\n}\n\n// ── 핸들러 ──\n\n/** 위젯 열기 */\nasync function open() {\n isOpen.value = true;\n // WS 연결 (아직 안 됐으면)\n if (!client.connected) {\n try { await client.connect(); } catch (e) { console.error(e); }\n }\n}\n\n/** 위젯 닫기 — 소켓 연결만 해제 (상담 종료는 상담원이 수행) */\nfunction close() {\n // 상담 중이면 소켓만 끊기 (세션은 유지 → 재접속 시 resumeSession으로 복구)\n if (view.value === 'chatting' || view.value === 'waiting') {\n client.disconnect();\n }\n isOpen.value = false;\n}\n\n/** 상담 입장 → 상담사 확인 */\nfunction handleEnter() {\n view.value = 'checking';\n client.checkAgents();\n}\n\n/** 환영 화면으로 복귀 */\nfunction goWelcome() {\n view.value = 'welcome';\n}\n\n/** 메시지 전송 */\nfunction handleSend() {\n if (!inputText.value.trim()) return;\n client.sendMessage(inputText.value, {\n userId: config._userId,\n username: config._username,\n nickname: config._nickname,\n });\n client.sendTyping(false); // 전송 시 타이핑 상태 초기화\n isTypingSent = false;\n if(typingTimer) clearTimeout(typingTimer);\n inputText.value = '';\n nextTick(() => scrollToBottom());\n}\n\nlet typingTimer = null;\nlet isTypingSent = false;\n/** 타이핑 이벤트 핸들러 */\nfunction handleTyping() {\n if (!isTypingSent) {\n client.sendTyping(true);\n isTypingSent = true;\n }\n\n if (typingTimer) clearTimeout(typingTimer);\n typingTimer = setTimeout(() => {\n client.sendTyping(false);\n isTypingSent = false;\n }, 2000);\n}\n\n/** 새 상담 시작 */\nfunction handleNewChat() {\n client.reset();\n view.value = 'welcome';\n}\n\n/** 메시지 CSS 클래스 */\nfunction msgCls(m) {\n return {\n 'hd-msg-mine': !m.isAgent && m.messageType !== 'system',\n 'hd-msg-agent': m.isAgent,\n 'hd-msg-sys-wrap': m.messageType === 'system',\n };\n}\n\n/** 시간 포맷 */\nfunction fmtTime(d) {\n if (!d) return '';\n const dt = new Date(d);\n return `${String(dt.getHours()).padStart(2, '0')}:${String(dt.getMinutes()).padStart(2, '0')}`;\n}\n\n// ── 메시지 감시 → 자동 스크롤 ──\nwatch(\n () => client.messages.length,\n () => nextTick(() => scrollToBottom()),\n);\n\n// ── Lifecycle ──\nonMounted(() => {\n client.on(UI_EVENTS.VIEW_CHANGE, onViewChange);\n client.on(UI_EVENTS.CONNECTION_CHANGE, onConnectionChange);\n\n // agentStatus 이벤트 → checking 후 available/unavailable 전환\n client.on(SERVER_EVENTS.AGENT_STATUS, (data) => {\n if (data.available) {\n // 상담사 있음 → 즉시 requestChat\n client.requestChat({\n userId: config._userId,\n subject: null,\n });\n }\n // unavailable은 _handleMessage에서 처리됨\n });\n\n // autoConnect\n if (config.autoConnect) {\n client.connect().catch(() => {});\n }\n});\n\nonUnmounted(() => {\n client.off(UI_EVENTS.VIEW_CHANGE, onViewChange);\n client.off(UI_EVENTS.CONNECTION_CHANGE, onConnectionChange);\n});\n</script>\n\n<style>\n/* ═══════════════════════════════════════\n HelpdeskWidget — 임베딩 위젯 스타일\n 다크/라이트 모드 자체 지원 (hd-theme-*)\n SCSS 없이 순수 CSS — 패키지 소비자에게 sass 의존성 불필요\n ═══════════════════════════════════════ */\n\n/* ── 테마 변수 ── */\n.hd-theme-dark {\n --hd-bg: #0c1219;\n --hd-bg2: #111b26;\n --hd-bg3: #162130;\n --hd-border: #1c2e42;\n --hd-txt: #b8d4e8;\n --hd-txt2: #8aaecd;\n --hd-txt3: #547a9c;\n --hd-acc: #00d9ff;\n --hd-acc-dim: rgba(0, 217, 255, 0.1);\n --hd-mine: #00d9ff;\n --hd-mine-fg: #000;\n --hd-agent-bg: #162130;\n --hd-agent-fg: #b8d4e8;\n --hd-shadow: rgba(0, 0, 0, 0.5);\n}\n\n.hd-theme-light {\n --hd-bg: #ffffff;\n --hd-bg2: #f5f7fa;\n --hd-bg3: #eaeff5;\n --hd-border: #d4dbe5;\n --hd-txt: #0f1923;\n --hd-txt2: #2d4050;\n --hd-txt3: #5a6d7e;\n --hd-acc: #0072a8;\n --hd-acc-dim: rgba(0, 114, 168, 0.1);\n --hd-mine: #0072a8;\n --hd-mine-fg: #fff;\n --hd-agent-bg: #eaeff5;\n --hd-agent-fg: #0f1923;\n --hd-shadow: rgba(0, 0, 0, 0.15);\n}\n\n/* ── 위치 ── */\n.hd-widget {\n position: fixed;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans KR', sans-serif;\n font-size: 14px;\n line-height: 1.5;\n}\n.hd-pos-bottom-right { bottom: 24px; right: 24px; }\n.hd-pos-bottom-left { bottom: 24px; left: 24px; }\n\n/* ── FAB ── */\n.hd-fab {\n width: 56px;\n height: 56px;\n border-radius: 50%;\n background: var(--hd-acc);\n color: var(--hd-mine-fg);\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n box-shadow: 0 4px 16px var(--hd-shadow);\n transition: transform 0.2s, box-shadow 0.2s;\n}\n.hd-fab:hover {\n transform: scale(1.08);\n box-shadow: 0 6px 24px var(--hd-shadow);\n}\n\n/* ── 패널 ── */\n.hd-panel {\n width: 360px;\n max-height: 520px;\n background: var(--hd-bg);\n border: 1px solid var(--hd-border);\n border-radius: 12px;\n box-shadow: 0 8px 32px var(--hd-shadow);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.hd-panel-hd {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 12px 16px;\n background: var(--hd-acc);\n color: var(--hd-mine-fg);\n}\n\n.hd-panel-title { font-weight: 700; font-size: 0.95rem; }\n\n.hd-panel-close {\n background: none;\n border: none;\n font-size: 1.3rem;\n color: inherit;\n cursor: pointer;\n opacity: 0.7;\n}\n.hd-panel-close:hover { opacity: 1; }\n\n/* ── 뷰 공통 ── */\n.hd-view {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 24px 20px;\n text-align: center;\n color: var(--hd-txt);\n min-height: 300px;\n}\n\n/* ── 환영 화면 ── */\n.hd-welcome-icon { font-size: 2.5rem; margin-bottom: 8px; }\n.hd-welcome-msg { font-size: 0.95rem; margin-bottom: 6px; color: var(--hd-txt); }\n.hd-welcome-hours { font-size: 0.78rem; color: var(--hd-txt3); margin-bottom: 16px; }\n.hd-welcome-hours i { margin-right: 4px; }\n\n.hd-btn-enter {\n width: 100%;\n padding: 10px;\n background: var(--hd-acc);\n color: var(--hd-mine-fg);\n border: none;\n border-radius: 8px;\n font-weight: 700;\n font-size: 0.9rem;\n cursor: pointer;\n transition: opacity 0.15s;\n}\n.hd-btn-enter:hover { opacity: 0.85; }\n\n/* ── 로딩 스피너 ── */\n.hd-spinner {\n width: 32px;\n height: 32px;\n border: 3px solid var(--hd-border);\n border-top-color: var(--hd-acc);\n border-radius: 50%;\n animation: hd-spin 0.8s linear infinite;\n margin-bottom: 12px;\n}\n@keyframes hd-spin { to { transform: rotate(360deg); } }\n\n/* ── 부재 화면 ── */\n.hd-unavail-icon { font-size: 2rem; margin-bottom: 8px; }\n.hd-unavail-msg { font-size: 0.9rem; color: var(--hd-txt); margin-bottom: 4px; }\n.hd-unavail-sub { font-size: 0.78rem; color: var(--hd-txt3); margin-bottom: 16px; }\n\n.hd-btn-retry, .hd-btn-back {\n width: 100%;\n padding: 8px;\n border-radius: 6px;\n font-size: 0.82rem;\n font-weight: 600;\n cursor: pointer;\n margin-bottom: 6px;\n transition: opacity 0.15s;\n}\n.hd-btn-retry:hover, .hd-btn-back:hover { opacity: 0.85; }\n.hd-btn-retry { background: var(--hd-acc); color: var(--hd-mine-fg); border: none; }\n.hd-btn-back { background: transparent; color: var(--hd-txt3); border: 1px solid var(--hd-border); }\n\n/* ── 대기 화면 ── */\n.hd-wait-msg { font-size: 0.9rem; color: var(--hd-txt); }\n.hd-wait-pos { font-size: 0.82rem; color: var(--hd-txt2); margin-top: 6px; }\n\n/* ── 채팅 화면 ── */\n.hd-view-chat { padding: 0; align-items: stretch; justify-content: flex-start; }\n\n.hd-chat-agent {\n padding: 6px 14px;\n font-size: 0.75rem;\n color: var(--hd-txt3);\n background: var(--hd-bg2);\n border-bottom: 1px solid var(--hd-border);\n}\n\n.hd-chat-msgs {\n flex: 1;\n overflow-y: auto;\n padding: 12px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n min-height: 200px;\n}\n\n.hd-msg { max-width: 85%; }\n.hd-msg-mine { align-self: flex-end; text-align: right; }\n.hd-msg-agent { align-self: flex-start; }\n.hd-msg-sys-wrap { align-self: center; }\n\n.hd-msg-sys {\n font-size: 0.7rem;\n color: var(--hd-txt3);\n background: var(--hd-bg3);\n padding: 2px 10px;\n border-radius: 999px;\n}\n\n.hd-msg-bubble {\n display: inline-block;\n padding: 8px 12px;\n border-radius: 12px;\n font-size: 0.85rem;\n line-height: 1.4;\n word-break: break-word;\n}\n\n.hd-msg-mine .hd-msg-bubble {\n background: var(--hd-mine);\n color: var(--hd-mine-fg);\n border-bottom-right-radius: 4px;\n}\n\n.hd-msg-agent .hd-msg-bubble {\n background: var(--hd-agent-bg);\n color: var(--hd-agent-fg);\n border-bottom-left-radius: 4px;\n}\n\n.hd-msg-time {\n font-size: 0.6rem;\n color: var(--hd-txt3);\n margin-top: 2px;\n opacity: 0.6;\n}\n\n/* ── 입력 ── */\n.hd-chat-input {\n display: flex;\n gap: 6px;\n padding: 8px 12px;\n border-top: 1px solid var(--hd-border);\n background: var(--hd-bg2);\n}\n\n.hd-chat-input input {\n flex: 1;\n padding: 7px 10px;\n border: 1px solid var(--hd-border);\n border-radius: 6px;\n font-size: 0.82rem;\n background: var(--hd-bg);\n color: var(--hd-txt);\n outline: none;\n}\n.hd-chat-input input:focus { border-color: var(--hd-acc); }\n.hd-chat-input input::placeholder { color: var(--hd-txt3); }\n\n.hd-btn-send {\n padding: 7px 10px;\n background: var(--hd-acc);\n color: var(--hd-mine-fg);\n border: none;\n border-radius: 6px;\n cursor: pointer;\n display: flex;\n align-items: center;\n}\n.hd-btn-send:hover { opacity: 0.85; }\n\n/* ── 종료 ── */\n.hd-chat-ended {\n padding: 12px;\n text-align: center;\n border-top: 1px solid var(--hd-border);\n background: var(--hd-bg2);\n}\n.hd-chat-ended p { font-size: 0.82rem; color: var(--hd-txt3); margin-bottom: 8px; }\n\n.hd-btn-new {\n padding: 6px 16px;\n background: var(--hd-acc-dim);\n color: var(--hd-acc);\n border: 1px solid var(--hd-acc);\n border-radius: 6px;\n font-size: 0.78rem;\n font-weight: 600;\n cursor: pointer;\n}\n.hd-btn-new:hover { background: var(--hd-acc); color: var(--hd-mine-fg); }\n\n.hd-slide-enter-active, .hd-slide-leave-active {\n transition: opacity 0.2s, transform 0.2s;\n}\n.hd-slide-enter-from, .hd-slide-leave-to {\n opacity: 0;\n transform: translateY(16px) scale(0.96);\n}\n\n/* ── Typing Indicator ── */\n.hd-typing-indicator {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n padding: 8px 12px;\n background-color: var(--hd-bg2);\n color: var(--hd-txt3);\n}\n.hd-typing-indicator span {\n display: block;\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background-color: currentColor;\n animation: hd-blink 1.4s infinite both;\n}\n.hd-typing-indicator span:nth-child(1) { animation-delay: -0.32s; }\n.hd-typing-indicator span:nth-child(2) { animation-delay: -0.16s; }\n@keyframes hd-blink {\n 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }\n 40% { transform: scale(1); opacity: 1; }\n}\n</style>\n","/**\n * HelpdeskPlugin — Vue 3 Plugin\n *\n * app.use(HelpdeskPlugin, options) 로 설치.\n * HelpdeskClient를 provide하고, HelpdeskWidget을 글로벌 컴포넌트 등록.\n *\n * @module vue/plugin\n */\nimport HelpdeskClient from '../core/HelpdeskClient.js';\nimport HelpdeskWidget from './HelpdeskWidget.vue';\nimport { reactive } from 'vue';\n\nexport default {\n /**\n * Vue Plugin 설치\n *\n * @param {import('vue').App} app - Vue 앱 인스턴스\n * @param {Object} options - 위젯 옵션 (HelpdeskConfig 참조)\n */\n install(app, options = {}) {\n // 코어 클라이언트 생성 후 Vue reactive로 감싸 반응성(Reactivity) 부여\n const rawClient = new HelpdeskClient(options);\n const client = reactive(rawClient);\n\n // provide로 주입 (HelpdeskWidget에서 inject로 접근)\n app.provide('helpdeskClient', client);\n app.provide('helpdeskConfig', client.config);\n\n // 글로벌 컴포넌트 등록\n app.component('HelpdeskWidget', HelpdeskWidget);\n\n // $helpdesk로 옵션 API에서도 접근 가능\n app.config.globalProperties.$helpdesk = client;\n },\n};\n\nexport { HelpdeskWidget, HelpdeskClient };\n","/**\n * HelpdeskEmbed — Vanilla JS 글로벌 API\n *\n * Vue 없이 일반 웹사이트에서 사용.\n * <script src=\"helpdesk.umd.js\"></script>\n * <script>FuzionXHelpdesk.HelpdeskEmbed.init({ wsUrl: 'wss://...' });</script>\n *\n * DOM 직접 조작으로 위젯을 렌더링한다.\n *\n * @module vanilla/HelpdeskEmbed\n */\nimport HelpdeskClient from '../core/HelpdeskClient.js';\nimport { UI_EVENTS, SERVER_EVENTS } from '../core/HelpdeskEvents.js';\n\n/** @type {HelpdeskClient|null} 글로벌 클라이언트 인스턴스 */\nlet _client = null;\n\n/** @type {HTMLElement|null} 위젯 루트 DOM */\nlet _root = null;\n\n/** @type {boolean} 위젯 열림 상태 */\nlet _isOpen = false;\n\n/** @type {string} 현재 화면 상태 */\nlet _view = 'welcome';\n\n/**\n * Helpdesk 위젯 초기화 + DOM 삽입\n *\n * @param {Object} opts - HelpdeskConfig 옵션\n * @returns {HelpdeskClient} 클라이언트 인스턴스\n */\nfunction init(opts) {\n if (_client) {\n console.warn('[HelpdeskEmbed] 이미 초기화됨');\n return _client;\n }\n\n _client = new HelpdeskClient(opts);\n\n // CSS 주입\n _injectStyles(_client.config);\n\n // FAB 버튼 생성\n _createFab(_client.config);\n\n // 이벤트 바인딩\n _client.on(UI_EVENTS.VIEW_CHANGE, (data) => {\n _view = data.view;\n _renderPanel();\n });\n\n _client.on(UI_EVENTS.CONNECTION_CHANGE, () => {\n _renderPanel();\n });\n\n _client.on(SERVER_EVENTS.AGENT_STATUS, (data) => {\n if (data.available) {\n _client.requestChat({\n userId: _client.config._userId,\n subject: null,\n });\n }\n });\n\n _client.on(SERVER_EVENTS.MESSAGE, () => {\n _renderPanel();\n _scrollToBottom();\n });\n\n return _client;\n}\n\n/** 위젯 제거 */\nfunction destroy() {\n if (_client) {\n _client.destroy();\n _client = null;\n }\n if (_root) {\n _root.remove();\n _root = null;\n }\n // CSS 제거\n const style = document.getElementById('hd-embed-styles');\n if (style) style.remove();\n}\n\n/** 클라이언트 인스턴스 반환 */\nfunction getClient() {\n return _client;\n}\n\n// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n// DOM 렌더링\n// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n/**\n * FAB 버튼 생성\n * @param {Object} config\n * @private\n */\nfunction _createFab(config) {\n _root = document.createElement('div');\n _root.id = 'hd-embed-root';\n _root.className = `hd-widget hd-pos-${config.position} hd-theme-${_getTheme(config)}`;\n _root.style.zIndex = config.zIndex;\n\n // FAB\n const fab = document.createElement('button');\n fab.className = 'hd-fab';\n fab.id = 'hd-fab-btn';\n fab.title = config.title;\n fab.innerHTML = `<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"/></svg>`;\n fab.onclick = () => _togglePanel();\n\n _root.appendChild(fab);\n document.body.appendChild(_root);\n}\n\n/**\n * 패널 토글\n * @private\n */\nasync function _togglePanel() {\n _isOpen = !_isOpen;\n\n if (_isOpen) {\n if (!_client.connected) {\n try { await _client.connect(); } catch (e) { console.error(e); }\n }\n _renderPanel();\n } else {\n _removePanel();\n }\n\n // FAB 표시 토글\n const fab = _root.querySelector('#hd-fab-btn');\n if (fab) fab.style.display = _isOpen ? 'none' : 'flex';\n}\n\n/**\n * 패널 HTML 렌더링\n * @private\n */\nfunction _renderPanel() {\n if (!_isOpen || !_root) return;\n\n // 기존 패널 제거\n let panel = _root.querySelector('.hd-panel');\n if (!panel) {\n panel = document.createElement('div');\n panel.className = 'hd-panel';\n _root.appendChild(panel);\n }\n\n const config = _client.config;\n\n // 헤더\n let html = `\n <div class=\"hd-panel-hd\">\n <span class=\"hd-panel-title\">${config.title}</span>\n <button class=\"hd-panel-close\" onclick=\"document.getElementById('hd-embed-root')._hdClose()\">&times;</button>\n </div>\n `;\n\n // 뷰 컨텐츠\n switch (_view) {\n case 'welcome':\n html += `\n <div class=\"hd-view hd-view-welcome\">\n <div class=\"hd-welcome-icon\">💬</div>\n <p class=\"hd-welcome-msg\">${config.welcomeMessage}</p>\n <p class=\"hd-welcome-hours\">🕐 ${config.operatingHours}${config.offDayNotice ? ' · ' + config.offDayNotice : ''}</p>\n <button class=\"hd-btn-enter\" id=\"hd-enter-btn\">상담 입장하기</button>\n </div>`;\n break;\n\n case 'checking':\n html += `\n <div class=\"hd-view hd-view-checking\">\n <div class=\"hd-spinner\"></div>\n <p>상담사를 확인하고 있습니다...</p>\n </div>`;\n break;\n\n case 'unavailable':\n html += `\n <div class=\"hd-view hd-view-unavail\">\n <div class=\"hd-unavail-icon\">😔</div>\n <p class=\"hd-unavail-msg\">현재 상담 가능한 상담사가 없습니다.</p>\n <p class=\"hd-unavail-sub\">운영시간: ${config.operatingHours}</p>\n <button class=\"hd-btn-retry\" id=\"hd-retry-btn\">다시 확인</button>\n <button class=\"hd-btn-back\" id=\"hd-back-btn\">돌아가기</button>\n </div>`;\n break;\n\n case 'waiting':\n html += `\n <div class=\"hd-view hd-view-waiting\">\n <div class=\"hd-spinner\"></div>\n <p class=\"hd-wait-msg\">상담사 연결 대기 중...</p>\n ${_client.queuePosition ? `<p class=\"hd-wait-pos\">대기 순번: <strong>${_client.queuePosition}</strong></p>` : ''}\n </div>`;\n break;\n\n case 'chatting':\n case 'closed':\n html += _renderChatView();\n break;\n }\n\n panel.innerHTML = html;\n\n // 이벤트 바인딩\n _bindPanelEvents();\n}\n\n/**\n * 채팅 뷰 HTML 생성\n * @returns {string}\n * @private\n */\nfunction _renderChatView() {\n let msgsHtml = '';\n for (const m of _client.messages) {\n if (m.messageType === 'system') {\n msgsHtml += `<div class=\"hd-msg hd-msg-sys-wrap\"><div class=\"hd-msg-sys\">${_esc(m.content)}</div></div>`;\n } else {\n const cls = m.isAgent ? 'hd-msg-agent' : 'hd-msg-mine';\n msgsHtml += `\n <div class=\"hd-msg ${cls}\">\n <div class=\"hd-msg-bubble\">${_esc(m.content)}</div>\n <div class=\"hd-msg-time\">${_fmtTime(m.createdAt)}</div>\n </div>`;\n }\n }\n\n let html = `<div class=\"hd-view hd-view-chat\">`;\n\n // 상담원 표시\n if (_client.agentName) {\n html += `<div class=\"hd-chat-agent\">상담원: <strong>${_esc(_client.agentName)}</strong></div>`;\n }\n\n // 메시지 영역\n html += `<div class=\"hd-chat-msgs\" id=\"hd-msgs-box\">${msgsHtml}</div>`;\n\n // 입력 or 종료\n if (_view === 'chatting') {\n html += `\n <div class=\"hd-chat-input\">\n <input type=\"text\" id=\"hd-msg-input\" placeholder=\"메시지를 입력하세요...\" />\n <button class=\"hd-btn-send\" id=\"hd-send-btn\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M2 21l21-9L2 3v7l15 2-15 2z\"/></svg>\n </button>\n </div>`;\n } else {\n html += `\n <div class=\"hd-chat-ended\">\n <p>상담이 종료되었습니다.</p>\n <button class=\"hd-btn-new\" id=\"hd-new-btn\">새 상담 시작</button>\n </div>`;\n }\n\n html += '</div>';\n return html;\n}\n\n/**\n * 패널 이벤트 바인딩 (innerHTML 교체 후)\n * @private\n */\nfunction _bindPanelEvents() {\n // 닫기\n _root._hdClose = () => {\n _isOpen = false;\n _removePanel();\n const fab = _root.querySelector('#hd-fab-btn');\n if (fab) fab.style.display = 'flex';\n };\n\n // 입장\n const enterBtn = document.getElementById('hd-enter-btn');\n if (enterBtn) {\n enterBtn.onclick = () => {\n _view = 'checking';\n _renderPanel();\n _client.checkAgents();\n };\n }\n\n // 재시도\n const retryBtn = document.getElementById('hd-retry-btn');\n if (retryBtn) {\n retryBtn.onclick = () => {\n _view = 'checking';\n _renderPanel();\n _client.checkAgents();\n };\n }\n\n // 돌아가기\n const backBtn = document.getElementById('hd-back-btn');\n if (backBtn) {\n backBtn.onclick = () => {\n _view = 'welcome';\n _renderPanel();\n };\n }\n\n // 메시지 전송\n const sendBtn = document.getElementById('hd-send-btn');\n const msgInput = document.getElementById('hd-msg-input');\n if (sendBtn && msgInput) {\n sendBtn.onclick = () => _sendFromInput(msgInput);\n msgInput.onkeyup = (e) => {\n if (e.key === 'Enter') _sendFromInput(msgInput);\n };\n // 입력 포커스\n msgInput.focus();\n }\n\n // 새 상담\n const newBtn = document.getElementById('hd-new-btn');\n if (newBtn) {\n newBtn.onclick = () => {\n _client.reset();\n _view = 'welcome';\n _renderPanel();\n };\n }\n\n _scrollToBottom();\n}\n\n/**\n * 입력 필드에서 메시지 전송\n * @param {HTMLInputElement} input\n * @private\n */\nfunction _sendFromInput(input) {\n const text = input.value.trim();\n if (!text) return;\n\n _client.sendMessage(text, {\n userId: _client.config._userId,\n username: _client.config._username,\n nickname: _client.config._nickname,\n });\n\n input.value = '';\n _renderPanel();\n}\n\n/**\n * 패널 제거\n * @private\n */\nfunction _removePanel() {\n const panel = _root?.querySelector('.hd-panel');\n if (panel) panel.remove();\n}\n\n/**\n * 메시지 자동 스크롤\n * @private\n */\nfunction _scrollToBottom() {\n const box = document.getElementById('hd-msgs-box');\n if (box) box.scrollTop = box.scrollHeight;\n}\n\n// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n// 유틸리티\n// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n/**\n * 테마 자동 감지\n * @param {Object} config\n * @returns {string}\n * @private\n */\nfunction _getTheme(config) {\n if (config.theme !== 'auto') return config.theme;\n return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n}\n\n/**\n * HTML 이스케이프 (XSS 방지)\n * @param {string} str\n * @returns {string}\n * @private\n */\nfunction _esc(str) {\n if (!str) return '';\n return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n}\n\n/**\n * 시간 포맷 (HH:MM)\n * @param {string} d\n * @returns {string}\n * @private\n */\nfunction _fmtTime(d) {\n if (!d) return '';\n const dt = new Date(d);\n return `${String(dt.getHours()).padStart(2, '0')}:${String(dt.getMinutes()).padStart(2, '0')}`;\n}\n\n/**\n * 위젯 CSS 동적 주입 (HelpdeskWidget.vue의 스타일과 동일)\n * @param {Object} config\n * @private\n */\nfunction _injectStyles(config) {\n if (document.getElementById('hd-embed-styles')) return;\n\n const style = document.createElement('style');\n style.id = 'hd-embed-styles';\n style.textContent = `\n /* ── 테마 변수 ── */\n .hd-theme-dark{--hd-bg:#0c1219;--hd-bg2:#111b26;--hd-bg3:#162130;--hd-border:#1c2e42;--hd-txt:#b8d4e8;--hd-txt2:#8aaecd;--hd-txt3:#547a9c;--hd-acc:#00d9ff;--hd-acc-dim:rgba(0,217,255,.1);--hd-mine:#00d9ff;--hd-mine-fg:#000;--hd-agent-bg:#162130;--hd-agent-fg:#b8d4e8;--hd-shadow:rgba(0,0,0,.5)}\n .hd-theme-light{--hd-bg:#fff;--hd-bg2:#f5f7fa;--hd-bg3:#eaeff5;--hd-border:#d4dbe5;--hd-txt:#0f1923;--hd-txt2:#2d4050;--hd-txt3:#5a6d7e;--hd-acc:#0072a8;--hd-acc-dim:rgba(0,114,168,.1);--hd-mine:#0072a8;--hd-mine-fg:#fff;--hd-agent-bg:#eaeff5;--hd-agent-fg:#0f1923;--hd-shadow:rgba(0,0,0,.15)}\n\n .hd-widget{position:fixed;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Noto Sans KR',sans-serif;font-size:14px;line-height:1.5}\n .hd-pos-bottom-right{bottom:24px;right:24px}\n .hd-pos-bottom-left{bottom:24px;left:24px}\n .hd-fab{width:56px;height:56px;border-radius:50%;background:var(--hd-acc);color:var(--hd-mine-fg);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px var(--hd-shadow);transition:transform .2s}\n .hd-fab:hover{transform:scale(1.08)}\n .hd-panel{width:360px;max-height:520px;background:var(--hd-bg);border:1px solid var(--hd-border);border-radius:12px;box-shadow:0 8px 32px var(--hd-shadow);display:flex;flex-direction:column;overflow:hidden;position:absolute;bottom:0;right:0}\n .hd-panel-hd{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:var(--hd-acc);color:var(--hd-mine-fg)}\n .hd-panel-title{font-weight:700;font-size:.95rem}\n .hd-panel-close{background:none;border:none;font-size:1.3rem;color:inherit;cursor:pointer;opacity:.7}\n .hd-panel-close:hover{opacity:1}\n .hd-view{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:24px 20px;text-align:center;color:var(--hd-txt);min-height:300px}\n .hd-welcome-icon{font-size:2.5rem;margin-bottom:8px}\n .hd-welcome-msg{font-size:.95rem;margin-bottom:6px}\n .hd-welcome-hours{font-size:.78rem;color:var(--hd-txt3);margin-bottom:16px}\n .hd-btn-enter{width:100%;padding:10px;background:var(--hd-acc);color:var(--hd-mine-fg);border:none;border-radius:8px;font-weight:700;font-size:.9rem;cursor:pointer}\n .hd-btn-enter:hover{opacity:.85}\n .hd-spinner{width:32px;height:32px;border:3px solid var(--hd-border);border-top-color:var(--hd-acc);border-radius:50%;animation:hd-spin .8s linear infinite;margin-bottom:12px}\n @keyframes hd-spin{to{transform:rotate(360deg)}}\n .hd-unavail-icon{font-size:2rem;margin-bottom:8px}\n .hd-unavail-msg{font-size:.9rem;margin-bottom:4px}\n .hd-unavail-sub{font-size:.78rem;color:var(--hd-txt3);margin-bottom:16px}\n .hd-btn-retry,.hd-btn-back{width:100%;padding:8px;border-radius:6px;font-size:.82rem;font-weight:600;cursor:pointer;margin-bottom:6px}\n .hd-btn-retry{background:var(--hd-acc);color:var(--hd-mine-fg);border:none}\n .hd-btn-back{background:transparent;color:var(--hd-txt3);border:1px solid var(--hd-border)}\n .hd-wait-msg{font-size:.9rem}\n .hd-wait-pos{font-size:.82rem;color:var(--hd-txt2);margin-top:6px}\n .hd-view-chat{padding:0;align-items:stretch;justify-content:flex-start}\n .hd-chat-agent{padding:6px 14px;font-size:.75rem;color:var(--hd-txt3);background:var(--hd-bg2);border-bottom:1px solid var(--hd-border)}\n .hd-chat-msgs{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:6px;min-height:200px;max-height:320px}\n .hd-msg{max-width:85%}.hd-msg-mine{align-self:flex-end;text-align:right}.hd-msg-agent{align-self:flex-start}.hd-msg-sys-wrap{align-self:center}\n .hd-msg-sys{font-size:.7rem;color:var(--hd-txt3);background:var(--hd-bg3);padding:2px 10px;border-radius:999px}\n .hd-msg-bubble{display:inline-block;padding:8px 12px;border-radius:12px;font-size:.85rem;line-height:1.4;word-break:break-word}\n .hd-msg-mine .hd-msg-bubble{background:var(--hd-mine);color:var(--hd-mine-fg);border-bottom-right-radius:4px}\n .hd-msg-agent .hd-msg-bubble{background:var(--hd-agent-bg);color:var(--hd-agent-fg);border-bottom-left-radius:4px}\n .hd-msg-time{font-size:.6rem;color:var(--hd-txt3);margin-top:2px;opacity:.6}\n .hd-chat-input{display:flex;gap:6px;padding:8px 12px;border-top:1px solid var(--hd-border);background:var(--hd-bg2)}\n .hd-chat-input input{flex:1;padding:7px 10px;border:1px solid var(--hd-border);border-radius:6px;font-size:.82rem;background:var(--hd-bg);color:var(--hd-txt);outline:none}\n .hd-chat-input input:focus{border-color:var(--hd-acc)}\n .hd-chat-input input::placeholder{color:var(--hd-txt3)}\n .hd-btn-send{padding:7px 10px;background:var(--hd-acc);color:var(--hd-mine-fg);border:none;border-radius:6px;cursor:pointer;display:flex;align-items:center}\n .hd-chat-ended{padding:12px;text-align:center;border-top:1px solid var(--hd-border);background:var(--hd-bg2)}\n .hd-chat-ended p{font-size:.82rem;color:var(--hd-txt3);margin:0 0 8px}\n .hd-btn-new{padding:6px 16px;background:var(--hd-acc-dim);color:var(--hd-acc);border:1px solid var(--hd-acc);border-radius:6px;font-size:.78rem;font-weight:600;cursor:pointer}\n .hd-btn-new:hover{background:var(--hd-acc);color:var(--hd-mine-fg)}\n `;\n document.head.appendChild(style);\n}\n\nexport { init, destroy, getClient };\n"],"names":["DEFAULTS","resolveConfig","userOpts","config","CLIENT_EVENTS","SERVER_EVENTS","UI_EVENTS","HelpdeskClient","opts","event","handler","_a","data","fn","e","_v","wasmJsUrl","wasmBgUrl","mod","wsProto","url","info","type","raw","msg","m","sessionKey","userId","subject","content","userInfo","trimmed","isTyping","view","props","__props","client","inject","isOpen","ref","inputText","msgBox","effectiveTheme","computed","onViewChange","nextTick","scrollToBottom","onConnectionChange","watch","u","open","close","handleEnter","goWelcome","handleSend","isTypingSent","typingTimer","handleTyping","handleNewChat","msgCls","fmtTime","d","dt","onMounted","onUnmounted","_createElementBlock","_normalizeClass","_unref","_normalizeStyle","_createElementVNode","_createVNode","_Transition","_openBlock","_hoisted_2","_hoisted_3","_hoisted_4","_toDisplayString","_hoisted_5","_cache","_hoisted_6","_hoisted_7","_createTextVNode","_hoisted_8","_hoisted_9","_hoisted_10","_hoisted_11","_hoisted_12","_hoisted_13","_hoisted_14","_hoisted_15","_Fragment","i","_hoisted_16","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","$event","_hoisted_21","plugin","app","options","rawClient","reactive","HelpdeskWidget","_client","_root","_isOpen","_view","init","_injectStyles","_createFab","_renderPanel","_scrollToBottom","destroy","style","getClient","_getTheme","fab","_togglePanel","_removePanel","panel","html","_renderChatView","_bindPanelEvents","msgsHtml","_esc","cls","_fmtTime","enterBtn","retryBtn","backBtn","sendBtn","msgInput","_sendFromInput","newBtn","input","text","box","str"],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ (function(h,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(h=typeof globalThis<"u"?globalThis:h||self,e(h.FuzionXHelpdesk={},h.Vue))})(this,(function(h,e){"use strict";const T={wsUrl:"",wsNamespace:"/support-chat",aspEnabled:!0,masterSecret:"",wasmUrl:"/wasm/fuzionx_client_wasm.js",template:"default",theme:"auto",position:"bottom-right",title:"고객상담",welcomeMessage:"안녕하세요! 무엇을 도와드릴까요?",operatingHours:"09:00~18:00",offDayNotice:"주말/공휴일 휴무",autoConnect:!1,authToken:"",popupMode:!1,i18n:null,zIndex:9990};function C(o={}){const t={...T,...o};return["light","dark","auto"].includes(t.theme)||(console.warn(`[fuzionx-helpdesk] 알 수 없는 theme: "${t.theme}", 기본값 "auto" 사용`),t.theme="auto"),["bottom-right","bottom-left"].includes(t.position)||(console.warn(`[fuzionx-helpdesk] 알 수 없는 position: "${t.position}", 기본값 "bottom-right" 사용`),t.position="bottom-right"),t}const f={CHECK_AGENTS:"checkAgents",REQUEST_CHAT:"requestChat",MESSAGE:"message",CLOSE_CHAT:"closeChat",TYPING:"typing",RESUME_SESSION:"resumeSession"},m={AGENT_STATUS:"agentStatus",CHAT_QUEUED:"chatQueued",AGENT_JOINED:"agentJoined",MESSAGE:"newMessage",AGENT_LEFT:"agentLeft",CHAT_CLOSED:"chatClosed",ERROR:"error",TYPING:"typing"},p={OPEN:"helpdesk:open",CLOSE:"helpdesk:close",VIEW_CHANGE:"helpdesk:viewChange",CONNECTION_CHANGE:"helpdesk:connectionChange"};class x{constructor(t){this.config=C(t),this._ws=null,this.connected=!1,this.sessionKey=null,this.viewState="welcome",this.messages=[],this.queuePosition=0,this.agentName=null,this.isAgentTyping=!1,this._typingTimer=null,this.agentStatus={available:!1,count:0},this.history=[],this._listeners=new Map,this._masterSecret=this.config.masterSecret||"",this._aspEnabled=this.config.aspEnabled}on(t,s){return this._listeners.has(t)||this._listeners.set(t,new Set),this._listeners.get(t).add(s),this}off(t,s){var a;(a=this._listeners.get(t))==null||a.delete(s)}_emit(t,s){var a;(a=this._listeners.get(t))==null||a.forEach(n=>{try{n(s)}catch(i){console.error(`[Helpdesk] 이벤트 에러 (${t}):`,i)}})}async connect(){if(this._ws&&this.connected)return;if(window._aspReady&&await window._aspReady,!window.FuzionXSocket)try{const n=Date.now(),i=this.config.wasmUrl||"/wasm/fuzionx_client_wasm.js",g=i.replace(".js","_bg.wasm"),_=await import(`${i}?v=${n}`);await _.default({module_or_path:`${g}?v=${n}`}),window.FuzionXSocket=_.FuzionXSocket}catch(n){console.error("[Helpdesk] WASM 로드 실패:",n);return}const t=location.protocol==="https:"?"wss:":"ws:",a=`${this.config.wsUrl||`${t}//${location.host}/ws`}${this.config.wsNamespace}`;this._ws=new window.FuzionXSocket(a,this._masterSecret,this._aspEnabled),this._ws.on("connect",()=>{this.connected=!0,this._emit(p.CONNECTION_CHANGE,{connected:!0}),console.log("[Helpdesk] WS 연결됨")}),this._ws.on("disconnect",n=>{this.connected=!1,this._emit(p.CONNECTION_CHANGE,{connected:!1}),console.log("[Helpdesk] WS 연결 해제:",n)}),this._ws.on("error",()=>{console.error("[Helpdesk] WS 에러")}),this._ws.on("welcome",n=>{console.log("[Helpdesk] welcome:",n)}),this._ws.on("message",n=>{this._handleMessage(n)}),this._ws.connect()}disconnect(){this._ws&&(this._ws.disconnect(),this._ws=null),this.connected=!1}_send(t,s={}){this._ws&&this._ws.is_connected()?this._ws.send(t,s):console.warn("[Helpdesk] WS 연결 안됨, 메시지 전송 불가:",t)}_handleMessage(t){let s;try{s=typeof t=="string"?JSON.parse(t):t}catch(i){console.error("[Helpdesk] 메시지 파싱 오류:",i);return}const{type:a,data:n}=s;switch(a){case m.AGENT_STATUS:this.agentStatus=n,n.available?this._setView("checking"):this._setView("unavailable"),this._emit(a,n);break;case m.CHAT_QUEUED:this.sessionKey=n.sessionKey,this.queuePosition=n.position,this.sessionKey&&localStorage.setItem("fx_support_session",this.sessionKey),n.messages&&n.messages.length>0&&(this.messages=n.messages.map(i=>({userId:i.user_id,userName:i.nickname||i.username||(i.role_code?"상담사":"나"),content:i.content,messageType:i.message_type||"text",createdAt:i.created_at,isAgent:i.role_code==="admin"||i.role_code==="developer"}))),this._setView("waiting"),this._emit(a,n);break;case m.AGENT_JOINED:this.sessionKey=n.sessionKey||this.sessionKey,this.agentName=n.nickname||n.username||n.agentName||"상담사",this.queuePosition=0,this.sessionKey&&localStorage.setItem("fx_support_session",this.sessionKey),this._setView("chatting"),n.messages&&n.messages.length>0?(this.messages=n.messages.map(i=>({userId:i.user_id,userName:i.nickname||i.username||(i.role_code?this.agentName:"나"),content:i.content,messageType:i.message_type||"text",createdAt:i.created_at,isAgent:i.role_code==="admin"||i.role_code==="developer"})),this._addSystemMessage("이전 대화 내역을 불러왔습니다.")):this._addSystemMessage("상담원이 입장했습니다."),this._emit(a,n);break;case m.TYPING:this.isAgentTyping=n.isTyping,this._typingTimer&&clearTimeout(this._typingTimer),this.isAgentTyping&&(this._typingTimer=setTimeout(()=>{this.isAgentTyping=!1},3e3)),this._emit(a,n);break;case m.MESSAGE:this.messages.push({userId:n.userId,userName:n.userName,content:n.content,messageType:n.messageType||"text",createdAt:n.createdAt,isAgent:!0}),this._emit(a,n);break;case m.AGENT_LEFT:this._addSystemMessage("상담원 연결이 해제되었습니다. 잠시 후 다시 연결됩니다."),this._setView("waiting"),this._emit(a,n);break;case m.CHAT_CLOSED:n&&n.messages&&n.messages.length>0&&(this.messages=n.messages.map(i=>({userId:i.user_id,userName:i.nickname||i.username||(i.role_code?"상담사":"나"),content:i.content,messageType:i.message_type||"text",createdAt:i.created_at,isAgent:i.role_code==="admin"||i.role_code==="developer"}))),this._addSystemMessage("상담이 종료되었습니다."),this._setView("closed"),this._emit(a,n);break;case"resumeFailed":localStorage.removeItem("fx_support_session"),this.sessionKey=null,this._setView("welcome"),this._emit(a,n);break;case m.ERROR:console.error("[Helpdesk] 서버 에러:",n.message),this._emit(a,n);break;default:this._emit(a,n)}}_handleServerEvent(t,s){this._handleMessage({type:t,data:s})}hasSavedSession(){return!!localStorage.getItem("fx_support_session")}resumeSession(){const t=localStorage.getItem("fx_support_session");t&&this._send(f.RESUME_SESSION,{sessionKey:t,userId:this.config._userId||null})}checkAgents(){this._send(f.CHECK_AGENTS,{})}requestChat({userId:t=null,subject:s=null}={}){this._requestChatLock||(this._requestChatLock=!0,this._send(f.REQUEST_CHAT,{userId:t,subject:s,sessionKey:this.sessionKey||localStorage.getItem("fx_support_session")}),setTimeout(()=>{this._requestChatLock=!1},1e3))}sendMessage(t,s={}){if(!this.sessionKey||!t.trim())return;const a=t.trim();this._send(f.MESSAGE,{sessionKey:this.sessionKey,content:a,userId:s.userId||null,username:s.username||null,nickname:s.nickname||null,roleCode:"member"}),this.messages.push({userId:s.userId,userName:s.nickname||s.username||"나",content:a,messageType:"text",createdAt:new Date().toISOString(),isAgent:!1,_isLocal:!0}),this._emit(p.VIEW_CHANGE,{view:this.viewState,messages:this.messages})}sendTyping(t){this.sessionKey&&this._send(f.TYPING,{sessionKey:this.sessionKey,isTyping:t})}closeChat(){this.sessionKey&&this._send(f.CLOSE_CHAT,{sessionKey:this.sessionKey})}_setView(t){this.viewState=t,this._emit(p.VIEW_CHANGE,{view:t,messages:this.messages})}_addSystemMessage(t){this.messages.push({content:t,messageType:"system",createdAt:new Date().toISOString()})}reset(){this.sessionKey=null,this.viewState="welcome",this.messages=[],this.queuePosition=0,this.agentName="",this.agentStatus={available:!1,count:0}}destroy(){this.disconnect(),this._listeners.clear(),this.reset()}}const O=["title"],$={key:0,class:"hd-panel"},G={class:"hd-panel-hd"},D={class:"hd-panel-title"},K={key:0,class:"hd-view hd-view-welcome"},U={class:"hd-welcome-msg"},P={class:"hd-welcome-hours"},q={key:0},L={key:1,class:"hd-view hd-view-checking"},R={key:2,class:"hd-view hd-view-unavail"},j={class:"hd-unavail-sub"},F={key:3,class:"hd-view hd-view-waiting"},W={key:0,class:"hd-wait-pos"},J={key:4,class:"hd-view hd-view-chat"},Q={key:0,class:"hd-chat-agent"},X={key:0,class:"hd-msg-sys"},Y={class:"hd-msg-bubble"},Z={class:"hd-msg-time"},ee={key:0,class:"hd-msg hd-msg-agent"},te={key:1,class:"hd-chat-input"},se={key:2,class:"hd-chat-ended"},v={__name:"HelpdeskWidget",props:{user:{type:Object,default:null}},setup(o){const t=o,s=e.inject("helpdeskClient"),a=e.inject("helpdeskConfig"),n=e.ref(!1),i=e.ref("welcome"),g=e.ref(""),_=e.ref(null),ue=e.computed(()=>{var l;return a.theme!=="auto"?a.theme:(l=window.matchMedia)!=null&&l.call(window,"(prefers-color-scheme: dark)").matches?"dark":"light"});function B(l){i.value=l.view,l.view==="chatting"&&e.nextTick(()=>S())}function H(l){l.connected&&s.hasSavedSession()&&(i.value="checking",s.resumeSession())}e.watch(()=>t.user,l=>{l&&l._id?(a._userId=l._id,a._username=l.username,a._nickname=l.nickname||l.name):(a._userId=null,a._username=null,a._nickname=null)},{immediate:!0});function S(){_.value&&(_.value.scrollTop=_.value.scrollHeight)}async function fe(){if(n.value=!0,!s.connected)try{await s.connect()}catch(l){console.error(l)}}function be(){(i.value==="chatting"||i.value==="waiting")&&s.disconnect(),n.value=!1}function z(){i.value="checking",s.checkAgents()}function _e(){i.value="welcome"}function M(){g.value.trim()&&(s.sendMessage(g.value,{userId:a._userId,username:a._username,nickname:a._nickname}),s.sendTyping(!1),E=!1,y&&clearTimeout(y),g.value="",e.nextTick(()=>S()))}let y=null,E=!1;function we(){E||(s.sendTyping(!0),E=!0),y&&clearTimeout(y),y=setTimeout(()=>{s.sendTyping(!1),E=!1},2e3)}function ke(){s.reset(),i.value="welcome"}function ye(l){return{"hd-msg-mine":!l.isAgent&&l.messageType!=="system","hd-msg-agent":l.isAgent,"hd-msg-sys-wrap":l.messageType==="system"}}function Ee(l){if(!l)return"";const r=new Date(l);return`${String(r.getHours()).padStart(2,"0")}:${String(r.getMinutes()).padStart(2,"0")}`}return e.watch(()=>s.messages.length,()=>e.nextTick(()=>S())),e.onMounted(()=>{s.on(p.VIEW_CHANGE,B),s.on(p.CONNECTION_CHANGE,H),s.on(m.AGENT_STATUS,l=>{l.available&&s.requestChat({userId:a._userId,subject:null})}),a.autoConnect&&s.connect().catch(()=>{})}),e.onUnmounted(()=>{s.off(p.VIEW_CHANGE,B),s.off(p.CONNECTION_CHANGE,H)}),(l,r)=>(e.openBlock(),e.createElementBlock("div",{class:e.normalizeClass(["hd-widget",`hd-pos-${e.unref(a).position}`,`hd-theme-${ue.value}`]),style:e.normalizeStyle({zIndex:e.unref(a).zIndex})},[n.value?e.createCommentVNode("",!0):(e.openBlock(),e.createElementBlock("button",{key:0,class:"hd-fab",onClick:fe,title:e.unref(a).title},[...r[1]||(r[1]=[e.createElementVNode("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e.createElementVNode("path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"})],-1)])],8,O)),e.createVNode(e.Transition,{name:"hd-slide"},{default:e.withCtx(()=>[n.value?(e.openBlock(),e.createElementBlock("div",$,[e.createElementVNode("div",G,[e.createElementVNode("span",D,e.toDisplayString(e.unref(a).title),1),e.createElementVNode("button",{class:"hd-panel-close",onClick:be},"×")]),i.value==="welcome"?(e.openBlock(),e.createElementBlock("div",K,[r[3]||(r[3]=e.createElementVNode("div",{class:"hd-welcome-icon"},"💬",-1)),e.createElementVNode("p",U,e.toDisplayString(e.unref(a).welcomeMessage),1),e.createElementVNode("p",P,[r[2]||(r[2]=e.createElementVNode("i",null,"🕐",-1)),e.createTextVNode(" "+e.toDisplayString(e.unref(a).operatingHours)+" ",1),e.unref(a).offDayNotice?(e.openBlock(),e.createElementBlock("span",q," · "+e.toDisplayString(e.unref(a).offDayNotice),1)):e.createCommentVNode("",!0)]),e.createElementVNode("button",{class:"hd-btn-enter",onClick:z},"상담 입장하기")])):i.value==="checking"?(e.openBlock(),e.createElementBlock("div",L,[...r[4]||(r[4]=[e.createElementVNode("div",{class:"hd-spinner"},null,-1),e.createElementVNode("p",null,"상담사를 확인하고 있습니다...",-1)])])):i.value==="unavailable"?(e.openBlock(),e.createElementBlock("div",R,[r[5]||(r[5]=e.createElementVNode("div",{class:"hd-unavail-icon"},"😔",-1)),r[6]||(r[6]=e.createElementVNode("p",{class:"hd-unavail-msg"},"현재 상담 가능한 상담사가 없습니다.",-1)),e.createElementVNode("p",j,"운영시간: "+e.toDisplayString(e.unref(a).operatingHours),1),e.createElementVNode("button",{class:"hd-btn-retry",onClick:z},"다시 확인"),e.createElementVNode("button",{class:"hd-btn-back",onClick:_e},"돌아가기")])):i.value==="waiting"?(e.openBlock(),e.createElementBlock("div",F,[r[8]||(r[8]=e.createElementVNode("div",{class:"hd-spinner"},null,-1)),r[9]||(r[9]=e.createElementVNode("p",{class:"hd-wait-msg"},"상담사 연결 대기 중...",-1)),e.unref(s).queuePosition?(e.openBlock(),e.createElementBlock("p",W,[r[7]||(r[7]=e.createTextVNode("대기 순번: ",-1)),e.createElementVNode("strong",null,e.toDisplayString(e.unref(s).queuePosition),1)])):e.createCommentVNode("",!0)])):i.value==="chatting"||i.value==="closed"?(e.openBlock(),e.createElementBlock("div",J,[e.unref(s).agentName?(e.openBlock(),e.createElementBlock("div",Q,[r[10]||(r[10]=e.createTextVNode(" 상담원: ",-1)),e.createElementVNode("strong",null,e.toDisplayString(e.unref(s).agentName),1)])):e.createCommentVNode("",!0),e.createElementVNode("div",{class:"hd-chat-msgs",ref_key:"msgBox",ref:_},[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(e.unref(s).messages,(w,xe)=>(e.openBlock(),e.createElementBlock("div",{key:xe,class:e.normalizeClass(["hd-msg",ye(w)])},[w.messageType==="system"?(e.openBlock(),e.createElementBlock("div",X,e.toDisplayString(w.content),1)):(e.openBlock(),e.createElementBlock(e.Fragment,{key:1},[e.createElementVNode("div",Y,e.toDisplayString(w.content),1),e.createElementVNode("div",Z,e.toDisplayString(Ee(w.createdAt)),1)],64))],2))),128)),e.unref(s).isAgentTyping?(e.openBlock(),e.createElementBlock("div",ee,[...r[11]||(r[11]=[e.createElementVNode("div",{class:"hd-msg-bubble hd-typing-indicator"},[e.createElementVNode("span",null,"."),e.createElementVNode("span",null,"."),e.createElementVNode("span",null,".")],-1)])])):e.createCommentVNode("",!0)],512),i.value==="chatting"?(e.openBlock(),e.createElementBlock("div",te,[e.withDirectives(e.createElementVNode("input",{type:"text","onUpdate:modelValue":r[0]||(r[0]=w=>g.value=w),onInput:we,onKeyup:e.withKeys(M,["enter"]),placeholder:"메시지를 입력하세요..."},null,544),[[e.vModelText,g.value]]),e.createElementVNode("button",{class:"hd-btn-send",onClick:M},[...r[12]||(r[12]=[e.createElementVNode("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"currentColor"},[e.createElementVNode("path",{d:"M2 21l21-9L2 3v7l15 2-15 2z"})],-1)])])])):e.createCommentVNode("",!0),i.value==="closed"?(e.openBlock(),e.createElementBlock("div",se,[r[13]||(r[13]=e.createElementVNode("p",null,"상담이 종료되었습니다.",-1)),e.createElementVNode("button",{class:"hd-btn-new",onClick:ke},"새 상담 시작")])):e.createCommentVNode("",!0)])):e.createCommentVNode("",!0)])):e.createCommentVNode("",!0)]),_:1})],6))}},ne={install(o,t={}){const s=new x(t),a=e.reactive(s);o.provide("helpdeskClient",a),o.provide("helpdeskConfig",a.config),o.component("HelpdeskWidget",v),o.config.globalProperties.$helpdesk=a}};let d=null,c=null,k=!1,b="welcome";function oe(o){return d?(console.warn("[HelpdeskEmbed] 이미 초기화됨"),d):(d=new x(o),ge(d.config),de(d.config),d.on(p.VIEW_CHANGE,t=>{b=t.view,u()}),d.on(p.CONNECTION_CHANGE,()=>{u()}),d.on(m.AGENT_STATUS,t=>{t.available&&d.requestChat({userId:d.config._userId,subject:null})}),d.on(m.MESSAGE,()=>{u(),I()}),d)}function ie(){d&&(d.destroy(),d=null),c&&(c.remove(),c=null);const o=document.getElementById("hd-embed-styles");o&&o.remove()}function ae(){return d}function de(o){c=document.createElement("div"),c.id="hd-embed-root",c.className=`hd-widget hd-pos-${o.position} hd-theme-${he(o)}`,c.style.zIndex=o.zIndex;const t=document.createElement("button");t.className="hd-fab",t.id="hd-fab-btn",t.title=o.title,t.innerHTML='<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',t.onclick=()=>re(),c.appendChild(t),document.body.appendChild(c)}async function re(){if(k=!k,k){if(!d.connected)try{await d.connect()}catch(t){console.error(t)}u()}else A();const o=c.querySelector("#hd-fab-btn");o&&(o.style.display=k?"none":"flex")}function u(){if(!k||!c)return;let o=c.querySelector(".hd-panel");o||(o=document.createElement("div"),o.className="hd-panel",c.appendChild(o));const t=d.config;let s=`
2
+ <div class="hd-panel-hd">
3
+ <span class="hd-panel-title">${t.title}</span>
4
+ <button class="hd-panel-close" onclick="document.getElementById('hd-embed-root')._hdClose()">&times;</button>
5
+ </div>
6
+ `;switch(b){case"welcome":s+=`
7
+ <div class="hd-view hd-view-welcome">
8
+ <div class="hd-welcome-icon">💬</div>
9
+ <p class="hd-welcome-msg">${t.welcomeMessage}</p>
10
+ <p class="hd-welcome-hours">🕐 ${t.operatingHours}${t.offDayNotice?" · "+t.offDayNotice:""}</p>
11
+ <button class="hd-btn-enter" id="hd-enter-btn">상담 입장하기</button>
12
+ </div>`;break;case"checking":s+=`
13
+ <div class="hd-view hd-view-checking">
14
+ <div class="hd-spinner"></div>
15
+ <p>상담사를 확인하고 있습니다...</p>
16
+ </div>`;break;case"unavailable":s+=`
17
+ <div class="hd-view hd-view-unavail">
18
+ <div class="hd-unavail-icon">😔</div>
19
+ <p class="hd-unavail-msg">현재 상담 가능한 상담사가 없습니다.</p>
20
+ <p class="hd-unavail-sub">운영시간: ${t.operatingHours}</p>
21
+ <button class="hd-btn-retry" id="hd-retry-btn">다시 확인</button>
22
+ <button class="hd-btn-back" id="hd-back-btn">돌아가기</button>
23
+ </div>`;break;case"waiting":s+=`
24
+ <div class="hd-view hd-view-waiting">
25
+ <div class="hd-spinner"></div>
26
+ <p class="hd-wait-msg">상담사 연결 대기 중...</p>
27
+ ${d.queuePosition?`<p class="hd-wait-pos">대기 순번: <strong>${d.queuePosition}</strong></p>`:""}
28
+ </div>`;break;case"chatting":case"closed":s+=le();break}o.innerHTML=s,ce()}function le(){let o="";for(const s of d.messages)if(s.messageType==="system")o+=`<div class="hd-msg hd-msg-sys-wrap"><div class="hd-msg-sys">${N(s.content)}</div></div>`;else{const a=s.isAgent?"hd-msg-agent":"hd-msg-mine";o+=`
29
+ <div class="hd-msg ${a}">
30
+ <div class="hd-msg-bubble">${N(s.content)}</div>
31
+ <div class="hd-msg-time">${me(s.createdAt)}</div>
32
+ </div>`}let t='<div class="hd-view hd-view-chat">';return d.agentName&&(t+=`<div class="hd-chat-agent">상담원: <strong>${N(d.agentName)}</strong></div>`),t+=`<div class="hd-chat-msgs" id="hd-msgs-box">${o}</div>`,b==="chatting"?t+=`
33
+ <div class="hd-chat-input">
34
+ <input type="text" id="hd-msg-input" placeholder="메시지를 입력하세요..." />
35
+ <button class="hd-btn-send" id="hd-send-btn">
36
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2 21l21-9L2 3v7l15 2-15 2z"/></svg>
37
+ </button>
38
+ </div>`:t+=`
39
+ <div class="hd-chat-ended">
40
+ <p>상담이 종료되었습니다.</p>
41
+ <button class="hd-btn-new" id="hd-new-btn">새 상담 시작</button>
42
+ </div>`,t+="</div>",t}function ce(){c._hdClose=()=>{k=!1,A();const g=c.querySelector("#hd-fab-btn");g&&(g.style.display="flex")};const o=document.getElementById("hd-enter-btn");o&&(o.onclick=()=>{b="checking",u(),d.checkAgents()});const t=document.getElementById("hd-retry-btn");t&&(t.onclick=()=>{b="checking",u(),d.checkAgents()});const s=document.getElementById("hd-back-btn");s&&(s.onclick=()=>{b="welcome",u()});const a=document.getElementById("hd-send-btn"),n=document.getElementById("hd-msg-input");a&&n&&(a.onclick=()=>V(n),n.onkeyup=g=>{g.key==="Enter"&&V(n)},n.focus());const i=document.getElementById("hd-new-btn");i&&(i.onclick=()=>{d.reset(),b="welcome",u()}),I()}function V(o){const t=o.value.trim();t&&(d.sendMessage(t,{userId:d.config._userId,username:d.config._username,nickname:d.config._nickname}),o.value="",u())}function A(){const o=c==null?void 0:c.querySelector(".hd-panel");o&&o.remove()}function I(){const o=document.getElementById("hd-msgs-box");o&&(o.scrollTop=o.scrollHeight)}function he(o){var t;return o.theme!=="auto"?o.theme:(t=window.matchMedia)!=null&&t.call(window,"(prefers-color-scheme: dark)").matches?"dark":"light"}function N(o){return o?o.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):""}function me(o){if(!o)return"";const t=new Date(o);return`${String(t.getHours()).padStart(2,"0")}:${String(t.getMinutes()).padStart(2,"0")}`}function ge(o){if(document.getElementById("hd-embed-styles"))return;const t=document.createElement("style");t.id="hd-embed-styles",t.textContent=`
43
+ /* ── 테마 변수 ── */
44
+ .hd-theme-dark{--hd-bg:#0c1219;--hd-bg2:#111b26;--hd-bg3:#162130;--hd-border:#1c2e42;--hd-txt:#b8d4e8;--hd-txt2:#8aaecd;--hd-txt3:#547a9c;--hd-acc:#00d9ff;--hd-acc-dim:rgba(0,217,255,.1);--hd-mine:#00d9ff;--hd-mine-fg:#000;--hd-agent-bg:#162130;--hd-agent-fg:#b8d4e8;--hd-shadow:rgba(0,0,0,.5)}
45
+ .hd-theme-light{--hd-bg:#fff;--hd-bg2:#f5f7fa;--hd-bg3:#eaeff5;--hd-border:#d4dbe5;--hd-txt:#0f1923;--hd-txt2:#2d4050;--hd-txt3:#5a6d7e;--hd-acc:#0072a8;--hd-acc-dim:rgba(0,114,168,.1);--hd-mine:#0072a8;--hd-mine-fg:#fff;--hd-agent-bg:#eaeff5;--hd-agent-fg:#0f1923;--hd-shadow:rgba(0,0,0,.15)}
46
+
47
+ .hd-widget{position:fixed;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Noto Sans KR',sans-serif;font-size:14px;line-height:1.5}
48
+ .hd-pos-bottom-right{bottom:24px;right:24px}
49
+ .hd-pos-bottom-left{bottom:24px;left:24px}
50
+ .hd-fab{width:56px;height:56px;border-radius:50%;background:var(--hd-acc);color:var(--hd-mine-fg);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px var(--hd-shadow);transition:transform .2s}
51
+ .hd-fab:hover{transform:scale(1.08)}
52
+ .hd-panel{width:360px;max-height:520px;background:var(--hd-bg);border:1px solid var(--hd-border);border-radius:12px;box-shadow:0 8px 32px var(--hd-shadow);display:flex;flex-direction:column;overflow:hidden;position:absolute;bottom:0;right:0}
53
+ .hd-panel-hd{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:var(--hd-acc);color:var(--hd-mine-fg)}
54
+ .hd-panel-title{font-weight:700;font-size:.95rem}
55
+ .hd-panel-close{background:none;border:none;font-size:1.3rem;color:inherit;cursor:pointer;opacity:.7}
56
+ .hd-panel-close:hover{opacity:1}
57
+ .hd-view{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:24px 20px;text-align:center;color:var(--hd-txt);min-height:300px}
58
+ .hd-welcome-icon{font-size:2.5rem;margin-bottom:8px}
59
+ .hd-welcome-msg{font-size:.95rem;margin-bottom:6px}
60
+ .hd-welcome-hours{font-size:.78rem;color:var(--hd-txt3);margin-bottom:16px}
61
+ .hd-btn-enter{width:100%;padding:10px;background:var(--hd-acc);color:var(--hd-mine-fg);border:none;border-radius:8px;font-weight:700;font-size:.9rem;cursor:pointer}
62
+ .hd-btn-enter:hover{opacity:.85}
63
+ .hd-spinner{width:32px;height:32px;border:3px solid var(--hd-border);border-top-color:var(--hd-acc);border-radius:50%;animation:hd-spin .8s linear infinite;margin-bottom:12px}
64
+ @keyframes hd-spin{to{transform:rotate(360deg)}}
65
+ .hd-unavail-icon{font-size:2rem;margin-bottom:8px}
66
+ .hd-unavail-msg{font-size:.9rem;margin-bottom:4px}
67
+ .hd-unavail-sub{font-size:.78rem;color:var(--hd-txt3);margin-bottom:16px}
68
+ .hd-btn-retry,.hd-btn-back{width:100%;padding:8px;border-radius:6px;font-size:.82rem;font-weight:600;cursor:pointer;margin-bottom:6px}
69
+ .hd-btn-retry{background:var(--hd-acc);color:var(--hd-mine-fg);border:none}
70
+ .hd-btn-back{background:transparent;color:var(--hd-txt3);border:1px solid var(--hd-border)}
71
+ .hd-wait-msg{font-size:.9rem}
72
+ .hd-wait-pos{font-size:.82rem;color:var(--hd-txt2);margin-top:6px}
73
+ .hd-view-chat{padding:0;align-items:stretch;justify-content:flex-start}
74
+ .hd-chat-agent{padding:6px 14px;font-size:.75rem;color:var(--hd-txt3);background:var(--hd-bg2);border-bottom:1px solid var(--hd-border)}
75
+ .hd-chat-msgs{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:6px;min-height:200px;max-height:320px}
76
+ .hd-msg{max-width:85%}.hd-msg-mine{align-self:flex-end;text-align:right}.hd-msg-agent{align-self:flex-start}.hd-msg-sys-wrap{align-self:center}
77
+ .hd-msg-sys{font-size:.7rem;color:var(--hd-txt3);background:var(--hd-bg3);padding:2px 10px;border-radius:999px}
78
+ .hd-msg-bubble{display:inline-block;padding:8px 12px;border-radius:12px;font-size:.85rem;line-height:1.4;word-break:break-word}
79
+ .hd-msg-mine .hd-msg-bubble{background:var(--hd-mine);color:var(--hd-mine-fg);border-bottom-right-radius:4px}
80
+ .hd-msg-agent .hd-msg-bubble{background:var(--hd-agent-bg);color:var(--hd-agent-fg);border-bottom-left-radius:4px}
81
+ .hd-msg-time{font-size:.6rem;color:var(--hd-txt3);margin-top:2px;opacity:.6}
82
+ .hd-chat-input{display:flex;gap:6px;padding:8px 12px;border-top:1px solid var(--hd-border);background:var(--hd-bg2)}
83
+ .hd-chat-input input{flex:1;padding:7px 10px;border:1px solid var(--hd-border);border-radius:6px;font-size:.82rem;background:var(--hd-bg);color:var(--hd-txt);outline:none}
84
+ .hd-chat-input input:focus{border-color:var(--hd-acc)}
85
+ .hd-chat-input input::placeholder{color:var(--hd-txt3)}
86
+ .hd-btn-send{padding:7px 10px;background:var(--hd-acc);color:var(--hd-mine-fg);border:none;border-radius:6px;cursor:pointer;display:flex;align-items:center}
87
+ .hd-chat-ended{padding:12px;text-align:center;border-top:1px solid var(--hd-border);background:var(--hd-bg2)}
88
+ .hd-chat-ended p{font-size:.82rem;color:var(--hd-txt3);margin:0 0 8px}
89
+ .hd-btn-new{padding:6px 16px;background:var(--hd-acc-dim);color:var(--hd-acc);border:1px solid var(--hd-acc);border-radius:6px;font-size:.78rem;font-weight:600;cursor:pointer}
90
+ .hd-btn-new:hover{background:var(--hd-acc);color:var(--hd-mine-fg)}
91
+ `,document.head.appendChild(t)}const pe=Object.freeze(Object.defineProperty({__proto__:null,destroy:ie,getClient:ae,init:oe},Symbol.toStringTag,{value:"Module"}));h.CLIENT_EVENTS=f,h.DEFAULTS=T,h.HelpdeskClient=x,h.HelpdeskEmbed=pe,h.HelpdeskPlugin=ne,h.HelpdeskWidget=v,h.SERVER_EVENTS=m,h.UI_EVENTS=p,h.resolveConfig=C,Object.defineProperty(h,Symbol.toStringTag,{value:"Module"})}));
92
+ //# sourceMappingURL=helpdesk.umd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpdesk.umd.js","sources":["../src/core/HelpdeskConfig.js","../src/core/HelpdeskEvents.js","../src/core/HelpdeskClient.js","../src/vue/HelpdeskWidget.vue","../src/vue/plugin.js","../src/vanilla/HelpdeskEmbed.js"],"sourcesContent":["/**\n * HelpdeskConfig — 옵션 기본값 및 검증\n *\n * HelpdeskClient / Vue Plugin / Vanilla Embed 에서 공통 사용.\n * 사용자 옵션을 기본값과 병합하고 필수값을 검증한다.\n *\n * @module core/HelpdeskConfig\n */\n\n/** @type {Object} 기본 옵션 */\nconst DEFAULTS = {\n /**\n * WebSocket URL (선택)\n * 빈 문자열이면 현재 도메인 기준 자동 구성.\n * 외부 사이트에서는 'wss://example.com/ws' 형태로 지정.\n */\n wsUrl: '',\n /** WS 네임스페이스 경로 */\n wsNamespace: '/support-chat',\n\n // ── ASP/WASM 설정 ──\n /**\n * ASP 암/복호화 활성화 여부\n * true: FuzionXSocket 내부에서 자동 ASP 암/복호화\n * false: 평문 통신 (개발/테스트용)\n */\n aspEnabled: true,\n /**\n * ASP masterSecret (동일 도메인 SPA에서 provide로 전달)\n * 빈 문자열이면 connect() 시 자동 추출 시도.\n */\n masterSecret: '',\n /**\n * WASM 파일 URL\n * 동일 도메인에서는 '/wasm/fuzionx_client_wasm.js' 자동 사용.\n * 외부 사이트에서는 CDN URL 지정 필요.\n */\n wasmUrl: '/wasm/fuzionx_client_wasm.js',\n\n /** 템플릿 이름 (default / minimal / bubble) */\n template: 'default',\n /** 테마 (light / dark / auto) */\n theme: 'auto',\n /** FAB 위치 (bottom-right / bottom-left) */\n position: 'bottom-right',\n /** 위젯 타이틀 */\n title: '고객상담',\n /** 입장안내 환영 메시지 */\n welcomeMessage: '안녕하세요! 무엇을 도와드릴까요?',\n /** 상담 가능 시간 안내 */\n operatingHours: '09:00~18:00',\n /** 주말/공휴일 안내 */\n offDayNotice: '주말/공휴일 휴무',\n /** WS 자동 연결 여부 */\n autoConnect: false,\n /** 인증 토큰 (외부 사이트 전용 JWT) */\n authToken: '',\n /** 팝업 모드 (true: window.open, false: 인라인 위젯) */\n popupMode: false,\n /** 다국어 메시지 오버라이드 */\n i18n: null,\n /** 위젯 z-index */\n zIndex: 9990,\n};\n\n/**\n * 옵션 병합 + 검증\n *\n * @param {Object} userOpts - 사용자 옵션\n * @returns {Object} 검증된 최종 옵션\n */\nexport function resolveConfig(userOpts = {}) {\n const config = { ...DEFAULTS, ...userOpts };\n\n // theme 검증\n if (!['light', 'dark', 'auto'].includes(config.theme)) {\n console.warn(`[fuzionx-helpdesk] 알 수 없는 theme: \"${config.theme}\", 기본값 \"auto\" 사용`);\n config.theme = 'auto';\n }\n\n // position 검증\n if (!['bottom-right', 'bottom-left'].includes(config.position)) {\n console.warn(`[fuzionx-helpdesk] 알 수 없는 position: \"${config.position}\", 기본값 \"bottom-right\" 사용`);\n config.position = 'bottom-right';\n }\n\n return config;\n}\n\nexport { DEFAULTS };\n","/**\n * HelpdeskEvents — 이벤트 상수 정의\n *\n * WebSocket 메시지 type 필드에 사용되는 이벤트명.\n * 서버(SupportChatHandler.js)와 동일한 값 유지 필수.\n *\n * @module core/HelpdeskEvents\n */\n\n/** 회원 → 서버 이벤트 */\nexport const CLIENT_EVENTS = {\n /** 상담사 온라인 여부 확인 (입장 버튼 클릭 시) */\n CHECK_AGENTS: 'checkAgents',\n /** 상담 요청 → 세션 생성 */\n REQUEST_CHAT: 'requestChat',\n /** 메시지 전송 */\n MESSAGE: 'message',\n /** 회원이 상담 종료 */\n CLOSE_CHAT: 'closeChat',\n /** 입력 중 상태 전송 */\n TYPING: 'typing',\n /** 과거 세션 복구 요청 */\n RESUME_SESSION: 'resumeSession',\n};\n\n/** 서버 → 회원 이벤트 */\nexport const SERVER_EVENTS = {\n /** 상담사 온라인 상태 응답 */\n AGENT_STATUS: 'agentStatus',\n /** 대기열 등록 완료 */\n CHAT_QUEUED: 'chatQueued',\n /** 상담원 입장 (첫 번째) */\n AGENT_JOINED: 'agentJoined',\n /** 메시지 수신 (FuzionXSocket 'message' 예약 이벤트 충돌 방지) */\n MESSAGE: 'newMessage',\n /** Primary agent 퇴장 */\n AGENT_LEFT: 'agentLeft',\n /** 상담 종료 */\n CHAT_CLOSED: 'chatClosed',\n /** 에러 */\n ERROR: 'error',\n /** 상대방의 입력 중 상태 수신 */\n TYPING: 'typing',\n};\n\n/** UI 내부 이벤트 (컴포넌트 간 통신용) */\nexport const UI_EVENTS = {\n /** 위젯 열기 */\n OPEN: 'helpdesk:open',\n /** 위젯 닫기 */\n CLOSE: 'helpdesk:close',\n /** 화면 전환: welcome → waiting → chat → closed */\n VIEW_CHANGE: 'helpdesk:viewChange',\n /** 연결 상태 변경 */\n CONNECTION_CHANGE: 'helpdesk:connectionChange',\n};\n","/**\n * HelpdeskClient — 고객 상담 WebSocket 코어 클라이언트\n *\n * WASM FuzionXSocket을 사용하여 ASP 암/복호화를 내부 처리.\n * useWebSocket.js 패턴을 참조하여 동일한 방식으로 WS 연결.\n *\n * 연결 흐름:\n * 1. window._aspReady 대기 (WASM 로드)\n * 2. FuzionXSocket 클래스 로드 (없으면 동적 import)\n * 3. new FuzionXSocket(url, masterSecret, aspEnabled) 생성\n * 4. socket.on('event', handler) 로 이벤트 수신\n * 5. socket.send(type, data) 로 메시지 전송\n *\n * @module core/HelpdeskClient\n */\nimport { resolveConfig } from './HelpdeskConfig.js';\nimport { CLIENT_EVENTS, SERVER_EVENTS, UI_EVENTS } from './HelpdeskEvents.js';\n\n/**\n * @typedef {'welcome'|'checking'|'unavailable'|'waiting'|'chatting'|'closed'} ViewState\n */\n\nexport default class HelpdeskClient {\n /**\n * @param {Object} opts - 사용자 옵션 (HelpdeskConfig 참조)\n */\n constructor(opts) {\n /** @type {Object} 검증된 옵션 */\n this.config = resolveConfig(opts);\n\n /** @type {Object|null} FuzionXSocket 인스턴스 (WASM) */\n this._ws = null;\n\n /** @type {boolean} WS 연결 상태 */\n this.connected = false;\n\n /** @type {string|null} 현재 세션 키 */\n this.sessionKey = null;\n\n /** @type {ViewState} 현재 화면 상태 */\n this.viewState = 'welcome';\n\n /** @type {Array<Object>} 메시지 목록 */\n this.messages = [];\n\n /** @type {number} 대기열 순번 */\n this.queuePosition = 0;\n\n /** @type {string} 상담원 이름 */\n this.agentName = null;\n\n /** @type {boolean} 상담사 타이핑 상태 */\n this.isAgentTyping = false;\n\n /** @type {number|null} 타이핑 상태 타이머 */\n this._typingTimer = null;\n\n /** @type {Object} 상담사 상태 (checkAgents 응답) */\n this.agentStatus = { available: false, count: 0 };\n\n /** @type {Array<Object>} 상담 이력 */\n this.history = [];\n\n /** @type {Map<string, Set<Function>>} 이벤트 리스너 */\n this._listeners = new Map();\n\n /** @type {string} masterSecret (ASP용) */\n this._masterSecret = this.config.masterSecret || '';\n\n /** @type {boolean} ASP 암/복호화 활성 여부 */\n this._aspEnabled = this.config.aspEnabled;\n }\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // EventEmitter\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n /**\n * 이벤트 리스너 등록\n *\n * @param {string} event - 이벤트명\n * @param {Function} handler - 핸들러 함수\n * @returns {HelpdeskClient} 체이닝용\n */\n on(event, handler) {\n if (!this._listeners.has(event)) {\n this._listeners.set(event, new Set());\n }\n this._listeners.get(event).add(handler);\n return this;\n }\n\n /**\n * 이벤트 리스너 제거\n *\n * @param {string} event - 이벤트명\n * @param {Function} handler - 핸들러 함수\n */\n off(event, handler) {\n this._listeners.get(event)?.delete(handler);\n }\n\n /**\n * 이벤트 발행\n *\n * @param {string} event - 이벤트명\n * @param {*} data - 페이로드\n * @private\n */\n _emit(event, data) {\n this._listeners.get(event)?.forEach(fn => {\n try { fn(data); } catch (e) { console.error(`[Helpdesk] 이벤트 에러 (${event}):`, e); }\n });\n }\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // WebSocket 연결 관리\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n /**\n * WS 연결 (FuzionXSocket 기반)\n *\n * useWebSocket.js connect() 패턴 동일:\n * 1. _aspReady 대기\n * 2. FuzionXSocket 클래스 로드\n * 3. new FuzionXSocket(url, masterSecret, aspEnabled)\n * 4. 이벤트 바인딩 + connect()\n *\n * @returns {Promise<void>}\n */\n async connect() {\n if (this._ws && this.connected) {\n return; // 이미 연결됨\n }\n\n // ── 1. WASM 로드 대기 (main.js에서 이미 초기화한 경우) ──\n if (window._aspReady) {\n await window._aspReady;\n }\n\n // ── 2. FuzionXSocket 클래스 로드 (없으면 동적 import) ──\n if (!window.FuzionXSocket) {\n try {\n const _v = Date.now(); // 캐시 방지\n const wasmJsUrl = this.config.wasmUrl || '/wasm/fuzionx_client_wasm.js';\n const wasmBgUrl = wasmJsUrl.replace('.js', '_bg.wasm');\n const mod = await import(/* @vite-ignore */ `${wasmJsUrl}?v=${_v}`);\n await mod.default({ module_or_path: `${wasmBgUrl}?v=${_v}` });\n window.FuzionXSocket = mod.FuzionXSocket;\n } catch (e) {\n console.error('[Helpdesk] WASM 로드 실패:', e);\n return;\n }\n }\n\n // ── 3. WS URL 구성 (/ws 접두사 + 네임스페이스) ──\n const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';\n const wsUrl = this.config.wsUrl || `${wsProto}//${location.host}/ws`;\n const url = `${wsUrl}${this.config.wsNamespace}`;\n\n // ── 4. FuzionXSocket 생성 ──\n // masterSecret: main.js에서 provide로 전달받은 값\n // aspEnabled: ASP 암/복호화 활성화 여부\n this._ws = new window.FuzionXSocket(url, this._masterSecret, this._aspEnabled);\n\n // ── 5. 이벤트 바인딩 (type별 on() 등록) ──\n this._ws.on('connect', () => {\n this.connected = true;\n this._emit(UI_EVENTS.CONNECTION_CHANGE, { connected: true });\n console.log('[Helpdesk] WS 연결됨');\n });\n\n this._ws.on('disconnect', (info) => {\n this.connected = false;\n this._emit(UI_EVENTS.CONNECTION_CHANGE, { connected: false });\n console.log('[Helpdesk] WS 연결 해제:', info);\n // 재연결은 WASM FuzionXSocket이 내부적으로 처리\n });\n\n this._ws.on('error', () => {\n console.error('[Helpdesk] WS 에러');\n });\n\n // welcome 이벤트 (서버 입장 확인)\n this._ws.on('welcome', (data) => {\n console.log('[Helpdesk] welcome:', data);\n });\n\n // 서버 메시지 수신부: FuzionXSocket은 WASM 내부에서 파싱된 JSON을 'message' 이벤트로 일괄 전달합니다.\n this._ws.on('message', (data) => {\n this._handleMessage(data);\n });\n\n // ── 6. 연결 ──\n this._ws.connect();\n }\n\n /** WS 연결 해제 */\n disconnect() {\n if (this._ws) {\n this._ws.disconnect();\n this._ws = null;\n }\n this.connected = false;\n }\n\n /**\n * WS 메시지 전송 (FuzionXSocket.send 사용)\n *\n * @param {string} type - 이벤트 타입\n * @param {Object} data - 페이로드\n * @private\n */\n _send(type, data = {}) {\n if (this._ws && this._ws.is_connected()) {\n this._ws.send(type, data);\n } else {\n console.warn('[Helpdesk] WS 연결 안됨, 메시지 전송 불가:', type);\n }\n }\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // WS 수신 메시지 처리\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n /**\n * 서버 메시지 핸들러\n *\n * FuzionXSocket의 'message' 이벤트에서 호출됨.\n * ASP 복호화는 FuzionXSocket 내부에서 자동 처리.\n *\n * @param {Object|string} raw - 서버 메시지 (자동 JSON 파싱 or 문자열)\n * @private\n */\n _handleMessage(raw) {\n let msg;\n try {\n msg = typeof raw === 'string' ? JSON.parse(raw) : raw;\n } catch (e) {\n console.error('[Helpdesk] 메시지 파싱 오류:', e);\n return;\n }\n\n const { type, data } = msg;\n\n switch (type) {\n case SERVER_EVENTS.AGENT_STATUS:\n // 상담사 가용 여부 응답\n this.agentStatus = data;\n if (data.available) {\n this._setView('checking');\n } else {\n this._setView('unavailable');\n }\n this._emit(type, data);\n break;\n\n case SERVER_EVENTS.CHAT_QUEUED:\n // 대기열 등록 완료\n this.sessionKey = data.sessionKey;\n this.queuePosition = data.position;\n if (this.sessionKey) {\n localStorage.setItem('fx_support_session', this.sessionKey);\n }\n \n if (data.messages && data.messages.length > 0) {\n this.messages = data.messages.map(m => ({\n userId: m.user_id,\n userName: m.nickname || m.username || (m.role_code ? '상담사' : '나'),\n content: m.content,\n messageType: m.message_type || 'text',\n createdAt: m.created_at,\n isAgent: m.role_code === 'admin' || m.role_code === 'developer'\n }));\n }\n\n this._setView('waiting');\n this._emit(type, data);\n break;\n\n case SERVER_EVENTS.AGENT_JOINED:\n // 상담원 입장 → 채팅 화면 전환\n this.sessionKey = data.sessionKey || this.sessionKey;\n this.agentName = data.nickname || data.username || data.agentName || '상담사';\n this.queuePosition = 0; // 대기 순번 초기화\n if (this.sessionKey) {\n localStorage.setItem('fx_support_session', this.sessionKey);\n }\n this._setView('chatting');\n \n if (data.messages && data.messages.length > 0) {\n this.messages = data.messages.map(m => ({\n userId: m.user_id,\n userName: m.nickname || m.username || (m.role_code ? this.agentName : '나'),\n content: m.content,\n messageType: m.message_type || 'text',\n createdAt: m.created_at,\n isAgent: m.role_code === 'admin' || m.role_code === 'developer'\n }));\n this._addSystemMessage('이전 대화 내역을 불러왔습니다.');\n } else {\n this._addSystemMessage('상담원이 입장했습니다.');\n }\n this._emit(type, data);\n break;\n\n case SERVER_EVENTS.TYPING:\n // 상담사가 입력 중일 때\n this.isAgentTyping = data.isTyping;\n if (this._typingTimer) clearTimeout(this._typingTimer);\n // 일정 시간 후 자동으로 타이핑 상태 제거 (방어탑)\n if (this.isAgentTyping) {\n this._typingTimer = setTimeout(() => {\n this.isAgentTyping = false;\n }, 3000);\n }\n this._emit(type, data);\n break;\n\n case SERVER_EVENTS.MESSAGE:\n // 실시간 메시지 수신 (상담원 메시지)\n this.messages.push({\n userId: data.userId,\n userName: data.userName,\n content: data.content,\n messageType: data.messageType || 'text',\n createdAt: data.createdAt,\n isAgent: true,\n });\n this._emit(type, data);\n break;\n\n case SERVER_EVENTS.AGENT_LEFT:\n // 상담원이 모두 나감 → 입력 차단 (waiting으로 전환)\n this._addSystemMessage('상담원 연결이 해제되었습니다. 잠시 후 다시 연결됩니다.');\n this._setView('waiting');\n this._emit(type, data);\n break;\n\n case SERVER_EVENTS.CHAT_CLOSED:\n // 상담 종료 (Continuous Chat을 위해 로컬 스토리지는 삭제하지 않음)\n if (data && data.messages && data.messages.length > 0) {\n this.messages = data.messages.map(m => ({\n userId: m.user_id,\n userName: m.nickname || m.username || (m.role_code ? '상담사' : '나'),\n content: m.content,\n messageType: m.message_type || 'text',\n createdAt: m.created_at,\n isAgent: m.role_code === 'admin' || m.role_code === 'developer'\n }));\n }\n this._addSystemMessage('상담이 종료되었습니다.');\n this._setView('closed');\n this._emit(type, data);\n break;\n\n case 'resumeFailed':\n // 세션 복구 실패 (종료되었거나 사라진 세션)\n localStorage.removeItem('fx_support_session');\n this.sessionKey = null;\n this._setView('welcome');\n this._emit(type, data);\n break;\n\n case SERVER_EVENTS.ERROR:\n console.error('[Helpdesk] 서버 에러:', data.message);\n this._emit(type, data);\n break;\n\n default:\n this._emit(type, data);\n }\n }\n\n /**\n * 서버 이벤트 직접 핸들러\n *\n * FuzionXSocket이 type별로 직접 dispatch하는 경우 처리.\n * 예: ws.on('agentStatus', data) → _handleServerEvent('agentStatus', data)\n *\n * @param {string} type - 이벤트 타입\n * @param {Object} data - 페이로드\n * @private\n */\n _handleServerEvent(type, data) {\n // { type, data } 형태로 래핑하여 _handleMessage에 위임\n this._handleMessage({ type, data });\n }\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 공개 API (회원 액션)\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n /**\n * 저장소에 과거 연결된 세션 키가 있는지 검사\n * @returns {boolean}\n */\n hasSavedSession() {\n return !!localStorage.getItem('fx_support_session');\n }\n\n /**\n * 이전 세션 정보로 다시 조인 시도\n * 서버에서 성공 시 agentJoined나 chatQueued 반환\n */\n resumeSession() {\n const sessionKey = localStorage.getItem('fx_support_session');\n if (!sessionKey) return;\n this._send(CLIENT_EVENTS.RESUME_SESSION, {\n sessionKey,\n userId: this.config._userId || null,\n });\n }\n\n /**\n * 상담사 가용 여부 확인\n *\n * \"상담 입장하기\" 클릭 시 호출.\n * 결과는 agentStatus 이벤트로 수신.\n */\n checkAgents() {\n this._send(CLIENT_EVENTS.CHECK_AGENTS, {});\n }\n\n /**\n * 상담 요청 (세션 생성)\n *\n * checkAgents → available=true 확인 후 호출.\n *\n * @param {Object} opts\n * @param {number} [opts.userId] - 회원 ID\n * @param {string} [opts.subject] - 상담 주제\n */\n requestChat({ userId = null, subject = null } = {}) {\n if (this._requestChatLock) return;\n this._requestChatLock = true;\n \n this._send(CLIENT_EVENTS.REQUEST_CHAT, {\n userId,\n subject,\n sessionKey: this.sessionKey || localStorage.getItem('fx_support_session'),\n });\n\n // 1초 후 락 해제 (네트워크 오류 재시도 허용)\n setTimeout(() => {\n this._requestChatLock = false;\n }, 1000);\n }\n\n /**\n * 메시지 전송\n *\n * @param {string} content - 메시지 내용\n * @param {Object} [userInfo] - 발신자 정보 { userId, username, nickname }\n */\n sendMessage(content, userInfo = {}) {\n if (!this.sessionKey || !content.trim()) return;\n\n const trimmed = content.trim();\n\n // FuzionXSocket.send(type, data)\n this._send(CLIENT_EVENTS.MESSAGE, {\n sessionKey: this.sessionKey,\n content: trimmed,\n userId: userInfo.userId || null,\n username: userInfo.username || null,\n nickname: userInfo.nickname || null,\n roleCode: 'member',\n });\n\n // 로컬 즉시 반영 (낙관적 업데이트)\n this.messages.push({\n userId: userInfo.userId,\n userName: userInfo.nickname || userInfo.username || '나',\n content: trimmed,\n messageType: 'text',\n createdAt: new Date().toISOString(),\n isAgent: false,\n _isLocal: true,\n });\n\n this._emit(UI_EVENTS.VIEW_CHANGE, { view: this.viewState, messages: this.messages });\n }\n\n /**\n * 타이핑 상태 전송\n * @param {boolean} isTyping - 입력 중 상태\n */\n sendTyping(isTyping) {\n if (!this.sessionKey) return;\n this._send(CLIENT_EVENTS.TYPING, {\n sessionKey: this.sessionKey,\n isTyping,\n });\n }\n\n /**\n * 상담 종료 (회원 측)\n */\n closeChat() {\n if (!this.sessionKey) return;\n this._send(CLIENT_EVENTS.CLOSE_CHAT, { sessionKey: this.sessionKey });\n }\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 내부 헬퍼\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n /**\n * 화면 상태 전환\n *\n * @param {ViewState} view\n * @private\n */\n _setView(view) {\n this.viewState = view;\n this._emit(UI_EVENTS.VIEW_CHANGE, { view, messages: this.messages });\n }\n\n /**\n * 시스템 메시지 추가\n *\n * @param {string} content\n * @private\n */\n _addSystemMessage(content) {\n this.messages.push({\n content,\n messageType: 'system',\n createdAt: new Date().toISOString(),\n });\n }\n\n /**\n * 상태 초기화 (새 상담 시작 전)\n */\n reset() {\n this.sessionKey = null;\n this.viewState = 'welcome';\n this.messages = [];\n this.queuePosition = 0;\n this.agentName = '';\n this.agentStatus = { available: false, count: 0 };\n }\n\n /** 리소스 정리 */\n destroy() {\n this.disconnect();\n this._listeners.clear();\n this.reset();\n }\n}\n","<template>\n <div :class=\"['hd-widget', `hd-pos-${config.position}`, `hd-theme-${effectiveTheme}`]\" :style=\"{ zIndex: config.zIndex }\">\n <!-- FAB (Floating Action Button) -->\n <button v-if=\"!isOpen\" class=\"hd-fab\" @click=\"open\" :title=\"config.title\">\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"/></svg>\n </button>\n\n <!-- 위젯 패널 -->\n <transition name=\"hd-slide\">\n <div v-if=\"isOpen\" class=\"hd-panel\">\n <!-- 패널 헤더 -->\n <div class=\"hd-panel-hd\">\n <span class=\"hd-panel-title\">{{ config.title }}</span>\n <button class=\"hd-panel-close\" @click=\"close\">&times;</button>\n </div>\n\n <!-- ── 환영 화면 (welcome) ── -->\n <div v-if=\"view === 'welcome'\" class=\"hd-view hd-view-welcome\">\n <div class=\"hd-welcome-icon\">💬</div>\n <p class=\"hd-welcome-msg\">{{ config.welcomeMessage }}</p>\n <p class=\"hd-welcome-hours\">\n <i>🕐</i> {{ config.operatingHours }}\n <span v-if=\"config.offDayNotice\"> · {{ config.offDayNotice }}</span>\n </p>\n <button class=\"hd-btn-enter\" @click=\"handleEnter\">상담 입장하기</button>\n </div>\n\n <!-- ── 상담사 확인 중 (checking) ── -->\n <div v-else-if=\"view === 'checking'\" class=\"hd-view hd-view-checking\">\n <div class=\"hd-spinner\"></div>\n <p>상담사를 확인하고 있습니다...</p>\n </div>\n\n <!-- ── 상담사 부재 (unavailable) ── -->\n <div v-else-if=\"view === 'unavailable'\" class=\"hd-view hd-view-unavail\">\n <div class=\"hd-unavail-icon\">😔</div>\n <p class=\"hd-unavail-msg\">현재 상담 가능한 상담사가 없습니다.</p>\n <p class=\"hd-unavail-sub\">운영시간: {{ config.operatingHours }}</p>\n <button class=\"hd-btn-retry\" @click=\"handleEnter\">다시 확인</button>\n <button class=\"hd-btn-back\" @click=\"goWelcome\">돌아가기</button>\n </div>\n\n <!-- ── 대기 중 (waiting) ── -->\n <div v-else-if=\"view === 'waiting'\" class=\"hd-view hd-view-waiting\">\n <div class=\"hd-spinner\"></div>\n <p class=\"hd-wait-msg\">상담사 연결 대기 중...</p>\n <p class=\"hd-wait-pos\" v-if=\"client.queuePosition\">대기 순번: <strong>{{ client.queuePosition }}</strong></p>\n </div>\n\n <!-- ── 채팅 (chatting) ── -->\n <div v-else-if=\"view === 'chatting' || view === 'closed'\" class=\"hd-view hd-view-chat\">\n <div class=\"hd-chat-agent\" v-if=\"client.agentName\">\n 상담원: <strong>{{ client.agentName }}</strong>\n </div>\n\n <!-- 메시지 영역 -->\n <div class=\"hd-chat-msgs\" ref=\"msgBox\">\n <div v-for=\"(m, i) in client.messages\" :key=\"i\" :class=\"['hd-msg', msgCls(m)]\">\n <div v-if=\"m.messageType === 'system'\" class=\"hd-msg-sys\">{{ m.content }}</div>\n <template v-else>\n <div class=\"hd-msg-bubble\">{{ m.content }}</div>\n <div class=\"hd-msg-time\">{{ fmtTime(m.createdAt) }}</div>\n </template>\n </div>\n <!-- 상담사 입력 중 표시 -->\n <div v-if=\"client.isAgentTyping\" class=\"hd-msg hd-msg-agent\">\n <div class=\"hd-msg-bubble hd-typing-indicator\">\n <span>.</span><span>.</span><span>.</span>\n </div>\n </div>\n </div>\n\n <!-- 입력 (chatting일 때만) -->\n <div class=\"hd-chat-input\" v-if=\"view === 'chatting'\">\n <input\n type=\"text\"\n v-model=\"inputText\"\n @input=\"handleTyping\"\n @keyup.enter=\"handleSend\"\n placeholder=\"메시지를 입력하세요...\"\n />\n <button class=\"hd-btn-send\" @click=\"handleSend\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M2 21l21-9L2 3v7l15 2-15 2z\"/></svg>\n </button>\n </div>\n\n <!-- 종료 상태 -->\n <div class=\"hd-chat-ended\" v-if=\"view === 'closed'\">\n <p>상담이 종료되었습니다.</p>\n <button class=\"hd-btn-new\" @click=\"handleNewChat\">새 상담 시작</button>\n </div>\n </div>\n </div>\n </transition>\n </div>\n</template>\n\n<script setup>\n/**\n * HelpdeskWidget.vue — 고객 상담 위젯 메인 컴포넌트\n *\n * FAB → 입장안내 → 상담사확인 → 대기 → 채팅 → 종료\n * HelpdeskClient 코어와 연동.\n */\nimport { ref, computed, watch, nextTick, onMounted, onUnmounted, inject } from 'vue';\nimport { UI_EVENTS, SERVER_EVENTS } from '../core/HelpdeskEvents.js';\n\nconst props = defineProps({\n user: {\n type: Object,\n default: null,\n }\n});\n\n/** HelpdeskClient는 Vue Plugin에서 provide로 주입 */\nconst client = inject('helpdeskClient');\n/** Config도 주입 */\nconst config = inject('helpdeskConfig');\n\n/** 위젯 열림 상태 */\nconst isOpen = ref(false);\n/** 현재 화면 상태 */\nconst view = ref('welcome');\n/** 메시지 입력 텍스트 */\nconst inputText = ref('');\n/** 메시지 스크롤 컨테이너 */\nconst msgBox = ref(null);\n\n/** 테마 자동 감지 */\nconst effectiveTheme = computed(() => {\n if (config.theme !== 'auto') return config.theme;\n // 브라우저 다크모드 감지\n return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n});\n\n// ── 이벤트 리스너 ──\nfunction onViewChange(data) {\n view.value = data.view;\n // 채팅 화면으로 전환 시 자동 스크롤\n if (data.view === 'chatting') {\n nextTick(() => scrollToBottom());\n }\n}\n\nfunction onConnectionChange(data) {\n if (data.connected && client.hasSavedSession()) {\n view.value = 'checking';\n client.resumeSession();\n }\n}\n\n// ── 유저 상태 동기화 ──\nwatch(() => props.user, (u) => {\n if (u && u._id) {\n config._userId = u._id;\n config._username = u.username;\n config._nickname = u.nickname || u.name;\n } else {\n config._userId = null;\n config._username = null;\n config._nickname = null;\n }\n}, { immediate: true });\n\n/** 자동 스크롤 */\nfunction scrollToBottom() {\n if (msgBox.value) {\n msgBox.value.scrollTop = msgBox.value.scrollHeight;\n }\n}\n\n// ── 핸들러 ──\n\n/** 위젯 열기 */\nasync function open() {\n isOpen.value = true;\n // WS 연결 (아직 안 됐으면)\n if (!client.connected) {\n try { await client.connect(); } catch (e) { console.error(e); }\n }\n}\n\n/** 위젯 닫기 — 소켓 연결만 해제 (상담 종료는 상담원이 수행) */\nfunction close() {\n // 상담 중이면 소켓만 끊기 (세션은 유지 → 재접속 시 resumeSession으로 복구)\n if (view.value === 'chatting' || view.value === 'waiting') {\n client.disconnect();\n }\n isOpen.value = false;\n}\n\n/** 상담 입장 → 상담사 확인 */\nfunction handleEnter() {\n view.value = 'checking';\n client.checkAgents();\n}\n\n/** 환영 화면으로 복귀 */\nfunction goWelcome() {\n view.value = 'welcome';\n}\n\n/** 메시지 전송 */\nfunction handleSend() {\n if (!inputText.value.trim()) return;\n client.sendMessage(inputText.value, {\n userId: config._userId,\n username: config._username,\n nickname: config._nickname,\n });\n client.sendTyping(false); // 전송 시 타이핑 상태 초기화\n isTypingSent = false;\n if(typingTimer) clearTimeout(typingTimer);\n inputText.value = '';\n nextTick(() => scrollToBottom());\n}\n\nlet typingTimer = null;\nlet isTypingSent = false;\n/** 타이핑 이벤트 핸들러 */\nfunction handleTyping() {\n if (!isTypingSent) {\n client.sendTyping(true);\n isTypingSent = true;\n }\n\n if (typingTimer) clearTimeout(typingTimer);\n typingTimer = setTimeout(() => {\n client.sendTyping(false);\n isTypingSent = false;\n }, 2000);\n}\n\n/** 새 상담 시작 */\nfunction handleNewChat() {\n client.reset();\n view.value = 'welcome';\n}\n\n/** 메시지 CSS 클래스 */\nfunction msgCls(m) {\n return {\n 'hd-msg-mine': !m.isAgent && m.messageType !== 'system',\n 'hd-msg-agent': m.isAgent,\n 'hd-msg-sys-wrap': m.messageType === 'system',\n };\n}\n\n/** 시간 포맷 */\nfunction fmtTime(d) {\n if (!d) return '';\n const dt = new Date(d);\n return `${String(dt.getHours()).padStart(2, '0')}:${String(dt.getMinutes()).padStart(2, '0')}`;\n}\n\n// ── 메시지 감시 → 자동 스크롤 ──\nwatch(\n () => client.messages.length,\n () => nextTick(() => scrollToBottom()),\n);\n\n// ── Lifecycle ──\nonMounted(() => {\n client.on(UI_EVENTS.VIEW_CHANGE, onViewChange);\n client.on(UI_EVENTS.CONNECTION_CHANGE, onConnectionChange);\n\n // agentStatus 이벤트 → checking 후 available/unavailable 전환\n client.on(SERVER_EVENTS.AGENT_STATUS, (data) => {\n if (data.available) {\n // 상담사 있음 → 즉시 requestChat\n client.requestChat({\n userId: config._userId,\n subject: null,\n });\n }\n // unavailable은 _handleMessage에서 처리됨\n });\n\n // autoConnect\n if (config.autoConnect) {\n client.connect().catch(() => {});\n }\n});\n\nonUnmounted(() => {\n client.off(UI_EVENTS.VIEW_CHANGE, onViewChange);\n client.off(UI_EVENTS.CONNECTION_CHANGE, onConnectionChange);\n});\n</script>\n\n<style>\n/* ═══════════════════════════════════════\n HelpdeskWidget — 임베딩 위젯 스타일\n 다크/라이트 모드 자체 지원 (hd-theme-*)\n SCSS 없이 순수 CSS — 패키지 소비자에게 sass 의존성 불필요\n ═══════════════════════════════════════ */\n\n/* ── 테마 변수 ── */\n.hd-theme-dark {\n --hd-bg: #0c1219;\n --hd-bg2: #111b26;\n --hd-bg3: #162130;\n --hd-border: #1c2e42;\n --hd-txt: #b8d4e8;\n --hd-txt2: #8aaecd;\n --hd-txt3: #547a9c;\n --hd-acc: #00d9ff;\n --hd-acc-dim: rgba(0, 217, 255, 0.1);\n --hd-mine: #00d9ff;\n --hd-mine-fg: #000;\n --hd-agent-bg: #162130;\n --hd-agent-fg: #b8d4e8;\n --hd-shadow: rgba(0, 0, 0, 0.5);\n}\n\n.hd-theme-light {\n --hd-bg: #ffffff;\n --hd-bg2: #f5f7fa;\n --hd-bg3: #eaeff5;\n --hd-border: #d4dbe5;\n --hd-txt: #0f1923;\n --hd-txt2: #2d4050;\n --hd-txt3: #5a6d7e;\n --hd-acc: #0072a8;\n --hd-acc-dim: rgba(0, 114, 168, 0.1);\n --hd-mine: #0072a8;\n --hd-mine-fg: #fff;\n --hd-agent-bg: #eaeff5;\n --hd-agent-fg: #0f1923;\n --hd-shadow: rgba(0, 0, 0, 0.15);\n}\n\n/* ── 위치 ── */\n.hd-widget {\n position: fixed;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans KR', sans-serif;\n font-size: 14px;\n line-height: 1.5;\n}\n.hd-pos-bottom-right { bottom: 24px; right: 24px; }\n.hd-pos-bottom-left { bottom: 24px; left: 24px; }\n\n/* ── FAB ── */\n.hd-fab {\n width: 56px;\n height: 56px;\n border-radius: 50%;\n background: var(--hd-acc);\n color: var(--hd-mine-fg);\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n box-shadow: 0 4px 16px var(--hd-shadow);\n transition: transform 0.2s, box-shadow 0.2s;\n}\n.hd-fab:hover {\n transform: scale(1.08);\n box-shadow: 0 6px 24px var(--hd-shadow);\n}\n\n/* ── 패널 ── */\n.hd-panel {\n width: 360px;\n max-height: 520px;\n background: var(--hd-bg);\n border: 1px solid var(--hd-border);\n border-radius: 12px;\n box-shadow: 0 8px 32px var(--hd-shadow);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.hd-panel-hd {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 12px 16px;\n background: var(--hd-acc);\n color: var(--hd-mine-fg);\n}\n\n.hd-panel-title { font-weight: 700; font-size: 0.95rem; }\n\n.hd-panel-close {\n background: none;\n border: none;\n font-size: 1.3rem;\n color: inherit;\n cursor: pointer;\n opacity: 0.7;\n}\n.hd-panel-close:hover { opacity: 1; }\n\n/* ── 뷰 공통 ── */\n.hd-view {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 24px 20px;\n text-align: center;\n color: var(--hd-txt);\n min-height: 300px;\n}\n\n/* ── 환영 화면 ── */\n.hd-welcome-icon { font-size: 2.5rem; margin-bottom: 8px; }\n.hd-welcome-msg { font-size: 0.95rem; margin-bottom: 6px; color: var(--hd-txt); }\n.hd-welcome-hours { font-size: 0.78rem; color: var(--hd-txt3); margin-bottom: 16px; }\n.hd-welcome-hours i { margin-right: 4px; }\n\n.hd-btn-enter {\n width: 100%;\n padding: 10px;\n background: var(--hd-acc);\n color: var(--hd-mine-fg);\n border: none;\n border-radius: 8px;\n font-weight: 700;\n font-size: 0.9rem;\n cursor: pointer;\n transition: opacity 0.15s;\n}\n.hd-btn-enter:hover { opacity: 0.85; }\n\n/* ── 로딩 스피너 ── */\n.hd-spinner {\n width: 32px;\n height: 32px;\n border: 3px solid var(--hd-border);\n border-top-color: var(--hd-acc);\n border-radius: 50%;\n animation: hd-spin 0.8s linear infinite;\n margin-bottom: 12px;\n}\n@keyframes hd-spin { to { transform: rotate(360deg); } }\n\n/* ── 부재 화면 ── */\n.hd-unavail-icon { font-size: 2rem; margin-bottom: 8px; }\n.hd-unavail-msg { font-size: 0.9rem; color: var(--hd-txt); margin-bottom: 4px; }\n.hd-unavail-sub { font-size: 0.78rem; color: var(--hd-txt3); margin-bottom: 16px; }\n\n.hd-btn-retry, .hd-btn-back {\n width: 100%;\n padding: 8px;\n border-radius: 6px;\n font-size: 0.82rem;\n font-weight: 600;\n cursor: pointer;\n margin-bottom: 6px;\n transition: opacity 0.15s;\n}\n.hd-btn-retry:hover, .hd-btn-back:hover { opacity: 0.85; }\n.hd-btn-retry { background: var(--hd-acc); color: var(--hd-mine-fg); border: none; }\n.hd-btn-back { background: transparent; color: var(--hd-txt3); border: 1px solid var(--hd-border); }\n\n/* ── 대기 화면 ── */\n.hd-wait-msg { font-size: 0.9rem; color: var(--hd-txt); }\n.hd-wait-pos { font-size: 0.82rem; color: var(--hd-txt2); margin-top: 6px; }\n\n/* ── 채팅 화면 ── */\n.hd-view-chat { padding: 0; align-items: stretch; justify-content: flex-start; }\n\n.hd-chat-agent {\n padding: 6px 14px;\n font-size: 0.75rem;\n color: var(--hd-txt3);\n background: var(--hd-bg2);\n border-bottom: 1px solid var(--hd-border);\n}\n\n.hd-chat-msgs {\n flex: 1;\n overflow-y: auto;\n padding: 12px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n min-height: 200px;\n}\n\n.hd-msg { max-width: 85%; }\n.hd-msg-mine { align-self: flex-end; text-align: right; }\n.hd-msg-agent { align-self: flex-start; }\n.hd-msg-sys-wrap { align-self: center; }\n\n.hd-msg-sys {\n font-size: 0.7rem;\n color: var(--hd-txt3);\n background: var(--hd-bg3);\n padding: 2px 10px;\n border-radius: 999px;\n}\n\n.hd-msg-bubble {\n display: inline-block;\n padding: 8px 12px;\n border-radius: 12px;\n font-size: 0.85rem;\n line-height: 1.4;\n word-break: break-word;\n}\n\n.hd-msg-mine .hd-msg-bubble {\n background: var(--hd-mine);\n color: var(--hd-mine-fg);\n border-bottom-right-radius: 4px;\n}\n\n.hd-msg-agent .hd-msg-bubble {\n background: var(--hd-agent-bg);\n color: var(--hd-agent-fg);\n border-bottom-left-radius: 4px;\n}\n\n.hd-msg-time {\n font-size: 0.6rem;\n color: var(--hd-txt3);\n margin-top: 2px;\n opacity: 0.6;\n}\n\n/* ── 입력 ── */\n.hd-chat-input {\n display: flex;\n gap: 6px;\n padding: 8px 12px;\n border-top: 1px solid var(--hd-border);\n background: var(--hd-bg2);\n}\n\n.hd-chat-input input {\n flex: 1;\n padding: 7px 10px;\n border: 1px solid var(--hd-border);\n border-radius: 6px;\n font-size: 0.82rem;\n background: var(--hd-bg);\n color: var(--hd-txt);\n outline: none;\n}\n.hd-chat-input input:focus { border-color: var(--hd-acc); }\n.hd-chat-input input::placeholder { color: var(--hd-txt3); }\n\n.hd-btn-send {\n padding: 7px 10px;\n background: var(--hd-acc);\n color: var(--hd-mine-fg);\n border: none;\n border-radius: 6px;\n cursor: pointer;\n display: flex;\n align-items: center;\n}\n.hd-btn-send:hover { opacity: 0.85; }\n\n/* ── 종료 ── */\n.hd-chat-ended {\n padding: 12px;\n text-align: center;\n border-top: 1px solid var(--hd-border);\n background: var(--hd-bg2);\n}\n.hd-chat-ended p { font-size: 0.82rem; color: var(--hd-txt3); margin-bottom: 8px; }\n\n.hd-btn-new {\n padding: 6px 16px;\n background: var(--hd-acc-dim);\n color: var(--hd-acc);\n border: 1px solid var(--hd-acc);\n border-radius: 6px;\n font-size: 0.78rem;\n font-weight: 600;\n cursor: pointer;\n}\n.hd-btn-new:hover { background: var(--hd-acc); color: var(--hd-mine-fg); }\n\n.hd-slide-enter-active, .hd-slide-leave-active {\n transition: opacity 0.2s, transform 0.2s;\n}\n.hd-slide-enter-from, .hd-slide-leave-to {\n opacity: 0;\n transform: translateY(16px) scale(0.96);\n}\n\n/* ── Typing Indicator ── */\n.hd-typing-indicator {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n padding: 8px 12px;\n background-color: var(--hd-bg2);\n color: var(--hd-txt3);\n}\n.hd-typing-indicator span {\n display: block;\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background-color: currentColor;\n animation: hd-blink 1.4s infinite both;\n}\n.hd-typing-indicator span:nth-child(1) { animation-delay: -0.32s; }\n.hd-typing-indicator span:nth-child(2) { animation-delay: -0.16s; }\n@keyframes hd-blink {\n 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }\n 40% { transform: scale(1); opacity: 1; }\n}\n</style>\n","/**\n * HelpdeskPlugin — Vue 3 Plugin\n *\n * app.use(HelpdeskPlugin, options) 로 설치.\n * HelpdeskClient를 provide하고, HelpdeskWidget을 글로벌 컴포넌트 등록.\n *\n * @module vue/plugin\n */\nimport HelpdeskClient from '../core/HelpdeskClient.js';\nimport HelpdeskWidget from './HelpdeskWidget.vue';\nimport { reactive } from 'vue';\n\nexport default {\n /**\n * Vue Plugin 설치\n *\n * @param {import('vue').App} app - Vue 앱 인스턴스\n * @param {Object} options - 위젯 옵션 (HelpdeskConfig 참조)\n */\n install(app, options = {}) {\n // 코어 클라이언트 생성 후 Vue reactive로 감싸 반응성(Reactivity) 부여\n const rawClient = new HelpdeskClient(options);\n const client = reactive(rawClient);\n\n // provide로 주입 (HelpdeskWidget에서 inject로 접근)\n app.provide('helpdeskClient', client);\n app.provide('helpdeskConfig', client.config);\n\n // 글로벌 컴포넌트 등록\n app.component('HelpdeskWidget', HelpdeskWidget);\n\n // $helpdesk로 옵션 API에서도 접근 가능\n app.config.globalProperties.$helpdesk = client;\n },\n};\n\nexport { HelpdeskWidget, HelpdeskClient };\n","/**\n * HelpdeskEmbed — Vanilla JS 글로벌 API\n *\n * Vue 없이 일반 웹사이트에서 사용.\n * <script src=\"helpdesk.umd.js\"></script>\n * <script>FuzionXHelpdesk.HelpdeskEmbed.init({ wsUrl: 'wss://...' });</script>\n *\n * DOM 직접 조작으로 위젯을 렌더링한다.\n *\n * @module vanilla/HelpdeskEmbed\n */\nimport HelpdeskClient from '../core/HelpdeskClient.js';\nimport { UI_EVENTS, SERVER_EVENTS } from '../core/HelpdeskEvents.js';\n\n/** @type {HelpdeskClient|null} 글로벌 클라이언트 인스턴스 */\nlet _client = null;\n\n/** @type {HTMLElement|null} 위젯 루트 DOM */\nlet _root = null;\n\n/** @type {boolean} 위젯 열림 상태 */\nlet _isOpen = false;\n\n/** @type {string} 현재 화면 상태 */\nlet _view = 'welcome';\n\n/**\n * Helpdesk 위젯 초기화 + DOM 삽입\n *\n * @param {Object} opts - HelpdeskConfig 옵션\n * @returns {HelpdeskClient} 클라이언트 인스턴스\n */\nfunction init(opts) {\n if (_client) {\n console.warn('[HelpdeskEmbed] 이미 초기화됨');\n return _client;\n }\n\n _client = new HelpdeskClient(opts);\n\n // CSS 주입\n _injectStyles(_client.config);\n\n // FAB 버튼 생성\n _createFab(_client.config);\n\n // 이벤트 바인딩\n _client.on(UI_EVENTS.VIEW_CHANGE, (data) => {\n _view = data.view;\n _renderPanel();\n });\n\n _client.on(UI_EVENTS.CONNECTION_CHANGE, () => {\n _renderPanel();\n });\n\n _client.on(SERVER_EVENTS.AGENT_STATUS, (data) => {\n if (data.available) {\n _client.requestChat({\n userId: _client.config._userId,\n subject: null,\n });\n }\n });\n\n _client.on(SERVER_EVENTS.MESSAGE, () => {\n _renderPanel();\n _scrollToBottom();\n });\n\n return _client;\n}\n\n/** 위젯 제거 */\nfunction destroy() {\n if (_client) {\n _client.destroy();\n _client = null;\n }\n if (_root) {\n _root.remove();\n _root = null;\n }\n // CSS 제거\n const style = document.getElementById('hd-embed-styles');\n if (style) style.remove();\n}\n\n/** 클라이언트 인스턴스 반환 */\nfunction getClient() {\n return _client;\n}\n\n// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n// DOM 렌더링\n// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n/**\n * FAB 버튼 생성\n * @param {Object} config\n * @private\n */\nfunction _createFab(config) {\n _root = document.createElement('div');\n _root.id = 'hd-embed-root';\n _root.className = `hd-widget hd-pos-${config.position} hd-theme-${_getTheme(config)}`;\n _root.style.zIndex = config.zIndex;\n\n // FAB\n const fab = document.createElement('button');\n fab.className = 'hd-fab';\n fab.id = 'hd-fab-btn';\n fab.title = config.title;\n fab.innerHTML = `<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"/></svg>`;\n fab.onclick = () => _togglePanel();\n\n _root.appendChild(fab);\n document.body.appendChild(_root);\n}\n\n/**\n * 패널 토글\n * @private\n */\nasync function _togglePanel() {\n _isOpen = !_isOpen;\n\n if (_isOpen) {\n if (!_client.connected) {\n try { await _client.connect(); } catch (e) { console.error(e); }\n }\n _renderPanel();\n } else {\n _removePanel();\n }\n\n // FAB 표시 토글\n const fab = _root.querySelector('#hd-fab-btn');\n if (fab) fab.style.display = _isOpen ? 'none' : 'flex';\n}\n\n/**\n * 패널 HTML 렌더링\n * @private\n */\nfunction _renderPanel() {\n if (!_isOpen || !_root) return;\n\n // 기존 패널 제거\n let panel = _root.querySelector('.hd-panel');\n if (!panel) {\n panel = document.createElement('div');\n panel.className = 'hd-panel';\n _root.appendChild(panel);\n }\n\n const config = _client.config;\n\n // 헤더\n let html = `\n <div class=\"hd-panel-hd\">\n <span class=\"hd-panel-title\">${config.title}</span>\n <button class=\"hd-panel-close\" onclick=\"document.getElementById('hd-embed-root')._hdClose()\">&times;</button>\n </div>\n `;\n\n // 뷰 컨텐츠\n switch (_view) {\n case 'welcome':\n html += `\n <div class=\"hd-view hd-view-welcome\">\n <div class=\"hd-welcome-icon\">💬</div>\n <p class=\"hd-welcome-msg\">${config.welcomeMessage}</p>\n <p class=\"hd-welcome-hours\">🕐 ${config.operatingHours}${config.offDayNotice ? ' · ' + config.offDayNotice : ''}</p>\n <button class=\"hd-btn-enter\" id=\"hd-enter-btn\">상담 입장하기</button>\n </div>`;\n break;\n\n case 'checking':\n html += `\n <div class=\"hd-view hd-view-checking\">\n <div class=\"hd-spinner\"></div>\n <p>상담사를 확인하고 있습니다...</p>\n </div>`;\n break;\n\n case 'unavailable':\n html += `\n <div class=\"hd-view hd-view-unavail\">\n <div class=\"hd-unavail-icon\">😔</div>\n <p class=\"hd-unavail-msg\">현재 상담 가능한 상담사가 없습니다.</p>\n <p class=\"hd-unavail-sub\">운영시간: ${config.operatingHours}</p>\n <button class=\"hd-btn-retry\" id=\"hd-retry-btn\">다시 확인</button>\n <button class=\"hd-btn-back\" id=\"hd-back-btn\">돌아가기</button>\n </div>`;\n break;\n\n case 'waiting':\n html += `\n <div class=\"hd-view hd-view-waiting\">\n <div class=\"hd-spinner\"></div>\n <p class=\"hd-wait-msg\">상담사 연결 대기 중...</p>\n ${_client.queuePosition ? `<p class=\"hd-wait-pos\">대기 순번: <strong>${_client.queuePosition}</strong></p>` : ''}\n </div>`;\n break;\n\n case 'chatting':\n case 'closed':\n html += _renderChatView();\n break;\n }\n\n panel.innerHTML = html;\n\n // 이벤트 바인딩\n _bindPanelEvents();\n}\n\n/**\n * 채팅 뷰 HTML 생성\n * @returns {string}\n * @private\n */\nfunction _renderChatView() {\n let msgsHtml = '';\n for (const m of _client.messages) {\n if (m.messageType === 'system') {\n msgsHtml += `<div class=\"hd-msg hd-msg-sys-wrap\"><div class=\"hd-msg-sys\">${_esc(m.content)}</div></div>`;\n } else {\n const cls = m.isAgent ? 'hd-msg-agent' : 'hd-msg-mine';\n msgsHtml += `\n <div class=\"hd-msg ${cls}\">\n <div class=\"hd-msg-bubble\">${_esc(m.content)}</div>\n <div class=\"hd-msg-time\">${_fmtTime(m.createdAt)}</div>\n </div>`;\n }\n }\n\n let html = `<div class=\"hd-view hd-view-chat\">`;\n\n // 상담원 표시\n if (_client.agentName) {\n html += `<div class=\"hd-chat-agent\">상담원: <strong>${_esc(_client.agentName)}</strong></div>`;\n }\n\n // 메시지 영역\n html += `<div class=\"hd-chat-msgs\" id=\"hd-msgs-box\">${msgsHtml}</div>`;\n\n // 입력 or 종료\n if (_view === 'chatting') {\n html += `\n <div class=\"hd-chat-input\">\n <input type=\"text\" id=\"hd-msg-input\" placeholder=\"메시지를 입력하세요...\" />\n <button class=\"hd-btn-send\" id=\"hd-send-btn\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M2 21l21-9L2 3v7l15 2-15 2z\"/></svg>\n </button>\n </div>`;\n } else {\n html += `\n <div class=\"hd-chat-ended\">\n <p>상담이 종료되었습니다.</p>\n <button class=\"hd-btn-new\" id=\"hd-new-btn\">새 상담 시작</button>\n </div>`;\n }\n\n html += '</div>';\n return html;\n}\n\n/**\n * 패널 이벤트 바인딩 (innerHTML 교체 후)\n * @private\n */\nfunction _bindPanelEvents() {\n // 닫기\n _root._hdClose = () => {\n _isOpen = false;\n _removePanel();\n const fab = _root.querySelector('#hd-fab-btn');\n if (fab) fab.style.display = 'flex';\n };\n\n // 입장\n const enterBtn = document.getElementById('hd-enter-btn');\n if (enterBtn) {\n enterBtn.onclick = () => {\n _view = 'checking';\n _renderPanel();\n _client.checkAgents();\n };\n }\n\n // 재시도\n const retryBtn = document.getElementById('hd-retry-btn');\n if (retryBtn) {\n retryBtn.onclick = () => {\n _view = 'checking';\n _renderPanel();\n _client.checkAgents();\n };\n }\n\n // 돌아가기\n const backBtn = document.getElementById('hd-back-btn');\n if (backBtn) {\n backBtn.onclick = () => {\n _view = 'welcome';\n _renderPanel();\n };\n }\n\n // 메시지 전송\n const sendBtn = document.getElementById('hd-send-btn');\n const msgInput = document.getElementById('hd-msg-input');\n if (sendBtn && msgInput) {\n sendBtn.onclick = () => _sendFromInput(msgInput);\n msgInput.onkeyup = (e) => {\n if (e.key === 'Enter') _sendFromInput(msgInput);\n };\n // 입력 포커스\n msgInput.focus();\n }\n\n // 새 상담\n const newBtn = document.getElementById('hd-new-btn');\n if (newBtn) {\n newBtn.onclick = () => {\n _client.reset();\n _view = 'welcome';\n _renderPanel();\n };\n }\n\n _scrollToBottom();\n}\n\n/**\n * 입력 필드에서 메시지 전송\n * @param {HTMLInputElement} input\n * @private\n */\nfunction _sendFromInput(input) {\n const text = input.value.trim();\n if (!text) return;\n\n _client.sendMessage(text, {\n userId: _client.config._userId,\n username: _client.config._username,\n nickname: _client.config._nickname,\n });\n\n input.value = '';\n _renderPanel();\n}\n\n/**\n * 패널 제거\n * @private\n */\nfunction _removePanel() {\n const panel = _root?.querySelector('.hd-panel');\n if (panel) panel.remove();\n}\n\n/**\n * 메시지 자동 스크롤\n * @private\n */\nfunction _scrollToBottom() {\n const box = document.getElementById('hd-msgs-box');\n if (box) box.scrollTop = box.scrollHeight;\n}\n\n// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n// 유틸리티\n// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n/**\n * 테마 자동 감지\n * @param {Object} config\n * @returns {string}\n * @private\n */\nfunction _getTheme(config) {\n if (config.theme !== 'auto') return config.theme;\n return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n}\n\n/**\n * HTML 이스케이프 (XSS 방지)\n * @param {string} str\n * @returns {string}\n * @private\n */\nfunction _esc(str) {\n if (!str) return '';\n return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n}\n\n/**\n * 시간 포맷 (HH:MM)\n * @param {string} d\n * @returns {string}\n * @private\n */\nfunction _fmtTime(d) {\n if (!d) return '';\n const dt = new Date(d);\n return `${String(dt.getHours()).padStart(2, '0')}:${String(dt.getMinutes()).padStart(2, '0')}`;\n}\n\n/**\n * 위젯 CSS 동적 주입 (HelpdeskWidget.vue의 스타일과 동일)\n * @param {Object} config\n * @private\n */\nfunction _injectStyles(config) {\n if (document.getElementById('hd-embed-styles')) return;\n\n const style = document.createElement('style');\n style.id = 'hd-embed-styles';\n style.textContent = `\n /* ── 테마 변수 ── */\n .hd-theme-dark{--hd-bg:#0c1219;--hd-bg2:#111b26;--hd-bg3:#162130;--hd-border:#1c2e42;--hd-txt:#b8d4e8;--hd-txt2:#8aaecd;--hd-txt3:#547a9c;--hd-acc:#00d9ff;--hd-acc-dim:rgba(0,217,255,.1);--hd-mine:#00d9ff;--hd-mine-fg:#000;--hd-agent-bg:#162130;--hd-agent-fg:#b8d4e8;--hd-shadow:rgba(0,0,0,.5)}\n .hd-theme-light{--hd-bg:#fff;--hd-bg2:#f5f7fa;--hd-bg3:#eaeff5;--hd-border:#d4dbe5;--hd-txt:#0f1923;--hd-txt2:#2d4050;--hd-txt3:#5a6d7e;--hd-acc:#0072a8;--hd-acc-dim:rgba(0,114,168,.1);--hd-mine:#0072a8;--hd-mine-fg:#fff;--hd-agent-bg:#eaeff5;--hd-agent-fg:#0f1923;--hd-shadow:rgba(0,0,0,.15)}\n\n .hd-widget{position:fixed;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Noto Sans KR',sans-serif;font-size:14px;line-height:1.5}\n .hd-pos-bottom-right{bottom:24px;right:24px}\n .hd-pos-bottom-left{bottom:24px;left:24px}\n .hd-fab{width:56px;height:56px;border-radius:50%;background:var(--hd-acc);color:var(--hd-mine-fg);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px var(--hd-shadow);transition:transform .2s}\n .hd-fab:hover{transform:scale(1.08)}\n .hd-panel{width:360px;max-height:520px;background:var(--hd-bg);border:1px solid var(--hd-border);border-radius:12px;box-shadow:0 8px 32px var(--hd-shadow);display:flex;flex-direction:column;overflow:hidden;position:absolute;bottom:0;right:0}\n .hd-panel-hd{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:var(--hd-acc);color:var(--hd-mine-fg)}\n .hd-panel-title{font-weight:700;font-size:.95rem}\n .hd-panel-close{background:none;border:none;font-size:1.3rem;color:inherit;cursor:pointer;opacity:.7}\n .hd-panel-close:hover{opacity:1}\n .hd-view{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:24px 20px;text-align:center;color:var(--hd-txt);min-height:300px}\n .hd-welcome-icon{font-size:2.5rem;margin-bottom:8px}\n .hd-welcome-msg{font-size:.95rem;margin-bottom:6px}\n .hd-welcome-hours{font-size:.78rem;color:var(--hd-txt3);margin-bottom:16px}\n .hd-btn-enter{width:100%;padding:10px;background:var(--hd-acc);color:var(--hd-mine-fg);border:none;border-radius:8px;font-weight:700;font-size:.9rem;cursor:pointer}\n .hd-btn-enter:hover{opacity:.85}\n .hd-spinner{width:32px;height:32px;border:3px solid var(--hd-border);border-top-color:var(--hd-acc);border-radius:50%;animation:hd-spin .8s linear infinite;margin-bottom:12px}\n @keyframes hd-spin{to{transform:rotate(360deg)}}\n .hd-unavail-icon{font-size:2rem;margin-bottom:8px}\n .hd-unavail-msg{font-size:.9rem;margin-bottom:4px}\n .hd-unavail-sub{font-size:.78rem;color:var(--hd-txt3);margin-bottom:16px}\n .hd-btn-retry,.hd-btn-back{width:100%;padding:8px;border-radius:6px;font-size:.82rem;font-weight:600;cursor:pointer;margin-bottom:6px}\n .hd-btn-retry{background:var(--hd-acc);color:var(--hd-mine-fg);border:none}\n .hd-btn-back{background:transparent;color:var(--hd-txt3);border:1px solid var(--hd-border)}\n .hd-wait-msg{font-size:.9rem}\n .hd-wait-pos{font-size:.82rem;color:var(--hd-txt2);margin-top:6px}\n .hd-view-chat{padding:0;align-items:stretch;justify-content:flex-start}\n .hd-chat-agent{padding:6px 14px;font-size:.75rem;color:var(--hd-txt3);background:var(--hd-bg2);border-bottom:1px solid var(--hd-border)}\n .hd-chat-msgs{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:6px;min-height:200px;max-height:320px}\n .hd-msg{max-width:85%}.hd-msg-mine{align-self:flex-end;text-align:right}.hd-msg-agent{align-self:flex-start}.hd-msg-sys-wrap{align-self:center}\n .hd-msg-sys{font-size:.7rem;color:var(--hd-txt3);background:var(--hd-bg3);padding:2px 10px;border-radius:999px}\n .hd-msg-bubble{display:inline-block;padding:8px 12px;border-radius:12px;font-size:.85rem;line-height:1.4;word-break:break-word}\n .hd-msg-mine .hd-msg-bubble{background:var(--hd-mine);color:var(--hd-mine-fg);border-bottom-right-radius:4px}\n .hd-msg-agent .hd-msg-bubble{background:var(--hd-agent-bg);color:var(--hd-agent-fg);border-bottom-left-radius:4px}\n .hd-msg-time{font-size:.6rem;color:var(--hd-txt3);margin-top:2px;opacity:.6}\n .hd-chat-input{display:flex;gap:6px;padding:8px 12px;border-top:1px solid var(--hd-border);background:var(--hd-bg2)}\n .hd-chat-input input{flex:1;padding:7px 10px;border:1px solid var(--hd-border);border-radius:6px;font-size:.82rem;background:var(--hd-bg);color:var(--hd-txt);outline:none}\n .hd-chat-input input:focus{border-color:var(--hd-acc)}\n .hd-chat-input input::placeholder{color:var(--hd-txt3)}\n .hd-btn-send{padding:7px 10px;background:var(--hd-acc);color:var(--hd-mine-fg);border:none;border-radius:6px;cursor:pointer;display:flex;align-items:center}\n .hd-chat-ended{padding:12px;text-align:center;border-top:1px solid var(--hd-border);background:var(--hd-bg2)}\n .hd-chat-ended p{font-size:.82rem;color:var(--hd-txt3);margin:0 0 8px}\n .hd-btn-new{padding:6px 16px;background:var(--hd-acc-dim);color:var(--hd-acc);border:1px solid var(--hd-acc);border-radius:6px;font-size:.78rem;font-weight:600;cursor:pointer}\n .hd-btn-new:hover{background:var(--hd-acc);color:var(--hd-mine-fg)}\n `;\n document.head.appendChild(style);\n}\n\nexport { init, destroy, getClient };\n"],"names":["DEFAULTS","resolveConfig","userOpts","config","CLIENT_EVENTS","SERVER_EVENTS","UI_EVENTS","HelpdeskClient","opts","event","handler","_a","data","fn","e","_v","wasmJsUrl","wasmBgUrl","mod","wsProto","url","info","type","raw","msg","m","sessionKey","userId","subject","content","userInfo","trimmed","isTyping","view","props","__props","client","inject","isOpen","ref","inputText","msgBox","effectiveTheme","computed","onViewChange","nextTick","scrollToBottom","onConnectionChange","watch","u","open","close","handleEnter","goWelcome","handleSend","isTypingSent","typingTimer","handleTyping","handleNewChat","msgCls","fmtTime","d","dt","onMounted","onUnmounted","_createElementBlock","_normalizeClass","_unref","_normalizeStyle","_createElementVNode","_createVNode","_Transition","_openBlock","_hoisted_2","_hoisted_3","_hoisted_4","_toDisplayString","_hoisted_5","_cache","_hoisted_6","_hoisted_7","_createTextVNode","_hoisted_8","_hoisted_9","_hoisted_10","_hoisted_11","_hoisted_12","_hoisted_13","_hoisted_14","_hoisted_15","_Fragment","i","_hoisted_16","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","$event","_hoisted_21","plugin","app","options","rawClient","reactive","HelpdeskWidget","_client","_root","_isOpen","_view","init","_injectStyles","_createFab","_renderPanel","_scrollToBottom","destroy","style","getClient","_getTheme","fab","_togglePanel","_removePanel","panel","html","_renderChatView","_bindPanelEvents","msgsHtml","_esc","cls","_fmtTime","enterBtn","retryBtn","backBtn","sendBtn","msgInput","_sendFromInput","newBtn","input","text","box","str"],"mappings":"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"}