UNPKG

debug-time-machine-core

Version:

๐Ÿ•ฐ๏ธ Debug Time Machine Core - ์‹œ๊ฐ„์—ฌํ–‰ ์—”์ง„๊ณผ ๋„คํŠธ์›Œํฌ ์ธํ„ฐ์…‰ํ„ฐ

1 lines โ€ข 303 kB
{"version":3,"sources":["../src/utils/index.ts","../src/network-interceptor/NetworkInterceptor.ts","../src/network-interceptor/AxiosInterceptor.ts","../src/network-interceptor/WebSocketConnector.ts","../src/network-interceptor/RealTimeAPIInterceptor.ts","../src/error-detector/ErrorDetector.ts","../src/error-detector/RealTimeErrorReporter.ts","../src/state-mapper/APIStateMapper.ts","../src/memory/MemoryManager.ts","../src/time-travel-engine/TimeTravelEngine.ts","../src/DebugTimeMachineEngine.ts","../src/error-detector/UserActionDetector.ts","../src/dom-snapshot/DOMSnapshotOptimizer.ts","../src/metrics/MetricsCollector.ts"],"sourcesContent":["/**\n * ๊ณ ์œ  ID ์ƒ์„ฑ ํ•จ์ˆ˜\n */\nexport function generateUniqueId(): string {\n return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n}\n\n/**\n * ๊นŠ์€ ๋ณต์‚ฌ ํ•จ์ˆ˜\n */\nexport function deepClone<T>(obj: T): T {\n if (obj === null || typeof obj !== 'object') {\n return obj;\n }\n\n if (obj instanceof Date) {\n return new Date(obj.getTime()) as unknown as T;\n }\n\n if (obj instanceof Array) {\n return obj.map(item => deepClone(item)) as unknown as T;\n }\n\n if (typeof obj === 'object') {\n const clonedObj = {} as T;\n for (const key in obj) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n clonedObj[key] = deepClone(obj[key]);\n }\n }\n return clonedObj;\n }\n\n return obj;\n}\n\n/**\n * ๋””๋ฐ”์šด์Šค ํ•จ์ˆ˜\n */\nexport function debounce<T extends (...args: unknown[]) => void>(\n func: T,\n wait: number\n): (...args: Parameters<T>) => void {\n let timeout: NodeJS.Timeout | null = null;\n\n return (...args: Parameters<T>): void => {\n if (timeout) {\n clearTimeout(timeout);\n }\n timeout = setTimeout(() => func(...args), wait);\n };\n}\n\n/**\n * ์“ฐ๋กœํ‹€ ํ•จ์ˆ˜\n */\nexport function throttle<T extends (...args: unknown[]) => void>(\n func: T,\n limit: number\n): (...args: Parameters<T>) => void {\n let inThrottle = false;\n\n return (...args: Parameters<T>): void => {\n if (!inThrottle) {\n func(...args);\n inThrottle = true;\n setTimeout(() => {\n inThrottle = false;\n }, limit);\n }\n };\n}\n\n/**\n * ๊ฐ์ฒด๊ฐ€ ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธ\n */\nexport function isEmptyObject(obj: Record<string, unknown>): boolean {\n return Object.keys(obj).length === 0;\n}\n\n/**\n * ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ์ฝ๊ธฐ ์‰ฌ์šด ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜\n */\nexport function formatTimestamp(timestamp: number): string {\n return new Date(timestamp).toISOString();\n}\n\n/**\n * ์ƒ๋Œ€์  ์‹œ๊ฐ„ ํ‘œ์‹œ\n */\nexport function getRelativeTime(timestamp: number): string {\n const now = Date.now();\n const diff = now - timestamp;\n \n const seconds = Math.floor(diff / 1000);\n const minutes = Math.floor(seconds / 60);\n const hours = Math.floor(minutes / 60);\n const days = Math.floor(hours / 24);\n\n if (days > 0) return `${days}์ผ ์ „`;\n if (hours > 0) return `${hours}์‹œ๊ฐ„ ์ „`;\n if (minutes > 0) return `${minutes}๋ถ„ ์ „`;\n return `${seconds}์ดˆ ์ „`;\n}\n","import { INetworkRequest, INetworkResponse, INetworkInterceptor } from '../types';\nimport { generateUniqueId } from '../utils';\nimport {\n INetworkTiming,\n IGraphQLInfo,\n IEnhancedNetworkRequest,\n IEnhancedNetworkResponse,\n INetworkInterceptorConfig\n} from './types';\n\n/**\n * ๊ณ ๊ธ‰ ๋„คํŠธ์›Œํฌ ์ธํ„ฐ์…‰ํ„ฐ\n * \n * Fetch, XHR์„ ์ง€์›ํ•˜๋ฉฐ ๋‹ค์Œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:\n * - ์ƒ์„ธํ•œ ํƒ€์ด๋ฐ ์ •๋ณด ์ˆ˜์ง‘\n * - GraphQL ์ฟผ๋ฆฌ ๊ฐ์ง€ ๋ฐ ํŒŒ์‹ฑ\n * - ์š”์ฒญ/์‘๋‹ต ํฌ๊ธฐ ์ธก์ •\n * - ๋„คํŠธ์›Œํฌ ์—๋Ÿฌ ์ถ”์ \n */\nexport class NetworkInterceptor implements INetworkInterceptor {\n private _config: Required<Omit<INetworkInterceptorConfig, 'onRequest' | 'onResponse' | 'onError'>> & {\n onRequest?: (request: IEnhancedNetworkRequest) => void;\n onResponse?: (response: IEnhancedNetworkResponse) => void;\n onError?: (error: Error, request: IEnhancedNetworkRequest) => void;\n };\n\n private _isIntercepting: boolean = false;\n private _requestHistory: IEnhancedNetworkRequest[] = [];\n private _responseHistory: IEnhancedNetworkResponse[] = [];\n\n // ์›๋ณธ ํ•จ์ˆ˜๋“ค ์ €์žฅ\n private _originalFetch: typeof fetch;\n private _originalXMLHttpRequest: typeof XMLHttpRequest;\n\n constructor(config: Partial<INetworkInterceptorConfig> = {}) {\n this._config = {\n maxHistorySize: 1000,\n captureRequestBody: true,\n captureResponseBody: true,\n enableTiming: true,\n enableGraphQLParsing: true,\n enableAxiosInterception: false,\n maxBodySize: 1024 * 1024, // 1MB\n excludePatterns: [\n /chrome-extension:/,\n /moz-extension:/,\n /webkit-masked-url:/,\n /^data:/,\n /\\.woff2?(\\?|$)/,\n /\\.ttf(\\?|$)/,\n /favicon\\.ico/,\n ],\n includePatterns: [],\n onRequest: undefined,\n onResponse: undefined,\n onError: undefined,\n ...config,\n };\n\n this._originalFetch = window.fetch.bind(window);\n this._originalXMLHttpRequest = window.XMLHttpRequest;\n }\n\n /**\n * ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๊ฐ€๋กœ์ฑ„๊ธฐ ์‹œ์ž‘\n */\n public startIntercepting(): void {\n if (this._isIntercepting) {\n console.warn('[NetworkInterceptor] Already intercepting');\n return;\n }\n\n console.log('[NetworkInterceptor] Starting network interception...');\n\n this._interceptFetch();\n this._interceptXMLHttpRequest();\n\n this._isIntercepting = true;\n console.log('[NetworkInterceptor] Network interception started');\n }\n\n /**\n * ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๊ฐ€๋กœ์ฑ„๊ธฐ ์ค‘๋‹จ\n */\n public stopIntercepting(): void {\n if (!this._isIntercepting) {\n return;\n }\n\n console.log('[NetworkInterceptor] Stopping network interception...');\n\n // ์›๋ณธ ํ•จ์ˆ˜๋“ค ๋ณต์›\n window.fetch = this._originalFetch;\n window.XMLHttpRequest = this._originalXMLHttpRequest;\n\n this._isIntercepting = false;\n console.log('[NetworkInterceptor] Network interception stopped');\n }\n\n /**\n * ์š”์ฒญ ํžˆ์Šคํ† ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ธฐ\n */\n public getRequestHistory(): IEnhancedNetworkRequest[] {\n return [...this._requestHistory];\n }\n\n /**\n * ์‘๋‹ต ํžˆ์Šคํ† ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ธฐ\n */\n public getResponseHistory(): IEnhancedNetworkResponse[] {\n return [...this._responseHistory];\n }\n\n /**\n * ํžˆ์Šคํ† ๋ฆฌ ์ดˆ๊ธฐํ™”\n */\n public clearHistory(): void {\n this._requestHistory = [];\n this._responseHistory = [];\n }\n\n /**\n * ๋„คํŠธ์›Œํฌ ์„ฑ๋Šฅ ๋งคํŠธ๋ฆญ ๊ฐ€์ ธ์˜ค๊ธฐ\n */\n public getNetworkMetrics() {\n const requests = this._requestHistory;\n const responses = this._responseHistory;\n\n if (requests.length === 0) {\n return {\n totalRequests: 0,\n successRate: 0,\n averageResponseTime: 0,\n totalDataTransferred: 0,\n errorCount: 0,\n slowRequestsCount: 0,\n };\n }\n\n const completedRequests = requests.filter(req => \n responses.some(res => res.requestId === req.id)\n );\n\n const totalDataTransferred = responses.reduce((sum, res) => \n sum + res.size.total, 0\n );\n\n const averageResponseTime = completedRequests.reduce((sum, req) => \n sum + req.timing.duration, 0\n ) / completedRequests.length;\n\n const errorCount = responses.filter(res => res.status >= 400).length;\n const slowRequestsCount = completedRequests.filter(req => \n req.timing.duration > 3000\n ).length;\n\n return {\n totalRequests: requests.length,\n completedRequests: completedRequests.length,\n successRate: completedRequests.length > 0 ? \n ((completedRequests.length - errorCount) / completedRequests.length) * 100 : 0,\n averageResponseTime: Math.round(averageResponseTime),\n totalDataTransferred,\n errorCount,\n slowRequestsCount,\n graphqlRequestsCount: requests.filter(req => req.graphql).length,\n };\n }\n\n /**\n * Fetch API ๊ฐ€๋กœ์ฑ„๊ธฐ\n */\n private _interceptFetch(): void {\n const self = this;\n \n window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n const url = typeof input === 'string' ? input : \n input instanceof URL ? input.toString() : input.url;\n\n // ์ œ์™ธ ํŒจํ„ด ํ™•์ธ\n if (self._shouldExcludeRequest(url)) {\n return self._originalFetch.call(this, input, init);\n }\n\n const requestId = generateUniqueId();\n const method = init?.method || 'GET';\n const startTime = performance.now();\n\n // ์š”์ฒญ ํ—ค๋” ์ถ”์ถœ\n const headers = self._extractHeaders(init?.headers);\n \n // ์š”์ฒญ ๋ฐ”๋”” ์ฒ˜๋ฆฌ\n let requestBody: string | undefined;\n let requestBodySize = 0;\n \n if (init?.body && self._config.captureRequestBody) {\n if (typeof init.body === 'string') {\n requestBody = init.body.length > self._config.maxBodySize ? \n init.body.substring(0, self._config.maxBodySize) + '...[truncated]' : \n init.body;\n requestBodySize = new TextEncoder().encode(init.body).length;\n } else {\n requestBody = '[Binary Data]';\n requestBodySize = (init.body as any)?.length || 0;\n }\n }\n\n // GraphQL ๊ฐ์ง€\n const graphqlInfo = self._parseGraphQL(headers, requestBody);\n\n // ํƒ€์ด๋ฐ ์ •๋ณด ์ดˆ๊ธฐํ™”\n const timing: INetworkTiming = {\n startTime,\n requestStart: startTime,\n responseStart: 0,\n responseEnd: 0,\n duration: 0,\n };\n\n // ์š”์ฒญ ๊ฐ์ฒด ์ƒ์„ฑ\n const networkRequest: IEnhancedNetworkRequest = {\n id: requestId,\n url,\n method,\n headers,\n body: requestBody,\n timestamp: Date.now(),\n timing,\n size: {\n requestHeaders: self._calculateHeadersSize(headers),\n requestBody: requestBodySize,\n total: self._calculateHeadersSize(headers) + requestBodySize,\n },\n graphql: graphqlInfo,\n priority: self._calculateRequestPriority(url, method, graphqlInfo),\n };\n\n self._requestHistory.push(networkRequest);\n\n // ํžˆ์Šคํ† ๋ฆฌ ํฌ๊ธฐ ์ œํ•œ\n if (self._requestHistory.length > self._config.maxHistorySize) {\n self._requestHistory = self._requestHistory.slice(-self._config.maxHistorySize);\n }\n\n // ์ฝœ๋ฐฑ ํ˜ธ์ถœ\n if (self._config.onRequest) {\n try {\n self._config.onRequest(networkRequest);\n } catch (error) {\n console.warn('[NetworkInterceptor] Error in onRequest callback:', error);\n }\n }\n\n try {\n timing.responseStart = performance.now();\n const response = await self._originalFetch.call(this, input, init);\n timing.responseEnd = performance.now();\n timing.duration = timing.responseEnd - timing.startTime;\n\n // ์‘๋‹ต ์ฒ˜๋ฆฌ\n await self._handleFetchResponse(response, networkRequest);\n\n return response;\n } catch (error) {\n timing.responseEnd = performance.now();\n timing.duration = timing.responseEnd - timing.startTime;\n\n // ์—๋Ÿฌ ์‘๋‹ต ์ƒ์„ฑ\n await self._handleFetchError(error as Error, networkRequest);\n\n throw error;\n }\n };\n }\n\n /**\n * XMLHttpRequest ๊ฐ€๋กœ์ฑ„๊ธฐ\n */\n private _interceptXMLHttpRequest(): void {\n const self = this;\n const OriginalXHR = this._originalXMLHttpRequest;\n\n (window as any).XMLHttpRequest = function XMLHttpRequestWrapper() {\n const xhr = new OriginalXHR();\n let requestId = '';\n let url = '';\n let method = '';\n let requestHeaders: Record<string, string> = {};\n let startTime = 0;\n\n const originalOpen = xhr.open;\n const originalSend = xhr.send;\n const originalSetRequestHeader = xhr.setRequestHeader;\n\n xhr.open = function(httpMethod: string, httpUrl: string | URL, ...args: unknown[]) {\n method = httpMethod;\n url = httpUrl.toString();\n requestId = generateUniqueId();\n \n return originalOpen.apply(this, [httpMethod, httpUrl, ...args] as Parameters<typeof originalOpen>);\n };\n\n xhr.setRequestHeader = function(name: string, value: string) {\n requestHeaders[name] = value;\n return originalSetRequestHeader.call(this, name, value);\n };\n\n xhr.send = function(body?: Document | XMLHttpRequestBodyInit | null) {\n // ์ œ์™ธ ํŒจํ„ด ํ™•์ธ\n if (self._shouldExcludeRequest(url)) {\n return originalSend.call(this, body);\n }\n\n startTime = performance.now();\n\n // ์š”์ฒญ ๋ฐ”๋”” ์ฒ˜๋ฆฌ\n let requestBody: string | undefined;\n let requestBodySize = 0;\n\n if (body && self._config.captureRequestBody) {\n if (typeof body === 'string') {\n requestBody = body.length > self._config.maxBodySize ? \n body.substring(0, self._config.maxBodySize) + '...[truncated]' : \n body;\n requestBodySize = new TextEncoder().encode(body).length;\n } else {\n requestBody = '[Binary Data]';\n requestBodySize = (body as any)?.length || 0;\n }\n }\n\n // GraphQL ๊ฐ์ง€\n const graphqlInfo = self._parseGraphQL(requestHeaders, requestBody);\n\n // ํƒ€์ด๋ฐ ์ •๋ณด\n const timing: INetworkTiming = {\n startTime,\n requestStart: startTime,\n responseStart: 0,\n responseEnd: 0,\n duration: 0,\n };\n\n const networkRequest: IEnhancedNetworkRequest = {\n id: requestId,\n url,\n method,\n headers: requestHeaders,\n body: requestBody,\n timestamp: Date.now(),\n timing,\n size: {\n requestHeaders: self._calculateHeadersSize(requestHeaders),\n requestBody: requestBodySize,\n total: self._calculateHeadersSize(requestHeaders) + requestBodySize,\n },\n graphql: graphqlInfo,\n priority: self._calculateRequestPriority(url, method, graphqlInfo),\n };\n\n self._requestHistory.push(networkRequest);\n\n // ์ฝœ๋ฐฑ ํ˜ธ์ถœ\n if (self._config.onRequest) {\n try {\n self._config.onRequest(networkRequest);\n } catch (error) {\n console.warn('[NetworkInterceptor] Error in onRequest callback:', error);\n }\n }\n\n // ์‘๋‹ต ์ฒ˜๋ฆฌ\n const originalOnReadyStateChange = xhr.onreadystatechange;\n xhr.onreadystatechange = function(event: Event) {\n if (xhr.readyState === 4) {\n timing.responseEnd = performance.now();\n timing.duration = timing.responseEnd - timing.startTime;\n\n self._handleXHRResponse(xhr, networkRequest);\n }\n\n if (originalOnReadyStateChange) {\n return originalOnReadyStateChange.call(this, event);\n }\n };\n\n return originalSend.call(this, body);\n };\n\n return xhr;\n } as any;\n\n // Static ์†์„ฑ๋“ค ๋ณต์‚ฌ\n Object.setPrototypeOf((window as any).XMLHttpRequest, OriginalXHR);\n Object.defineProperty((window as any).XMLHttpRequest, 'prototype', {\n value: OriginalXHR.prototype,\n writable: false\n });\n }\n\n /**\n * ์š”์ฒญ URL์ด ์ œ์™ธ ํŒจํ„ด์— ๋งค์น˜๋˜๋Š”์ง€ ํ™•์ธ\n */\n private _shouldExcludeRequest(url: string): boolean {\n return this._config.excludePatterns.some(pattern => pattern.test(url));\n }\n\n /**\n * GraphQL ์ฟผ๋ฆฌ ํŒŒ์‹ฑ\n */\n private _parseGraphQL(headers: Record<string, string>, body?: string): IGraphQLInfo | undefined {\n if (!this._config.enableGraphQLParsing || !body) {\n return undefined;\n }\n\n try {\n // Content-Type์ด GraphQL์ธ์ง€ ํ™•์ธ\n const contentType = headers['content-type'] || headers['Content-Type'] || '';\n if (contentType.includes('application/graphql')) {\n // Raw GraphQL query\n return {\n operationType: this._detectGraphQLOperationType(body),\n operationName: this._extractGraphQLOperationName(body),\n };\n }\n\n // JSON ํ˜•ํƒœ์˜ GraphQL ์š”์ฒญ์ธ์ง€ ํ™•์ธ\n if (contentType.includes('application/json')) {\n const parsed = JSON.parse(body);\n if (parsed.query || parsed.operationName) {\n return {\n operationType: this._detectGraphQLOperationType(parsed.query),\n operationName: parsed.operationName,\n variables: parsed.variables,\n extensions: parsed.extensions,\n };\n }\n }\n\n return undefined;\n } catch (error) {\n return undefined;\n }\n }\n\n /**\n * GraphQL ์˜คํผ๋ ˆ์ด์…˜ ํƒ€์ž… ๊ฐ์ง€\n */\n private _detectGraphQLOperationType(query: string): 'query' | 'mutation' | 'subscription' {\n if (!query) return 'query';\n\n const trimmed = query.trim().toLowerCase();\n if (trimmed.startsWith('mutation')) return 'mutation';\n if (trimmed.startsWith('subscription')) return 'subscription';\n return 'query';\n }\n\n /**\n * GraphQL ์˜คํผ๋ ˆ์ด์…˜ ์ด๋ฆ„ ์ถ”์ถœ\n */\n private _extractGraphQLOperationName(query: string): string | undefined {\n if (!query) return undefined;\n\n const match = query.match(/(?:query|mutation|subscription)\\s+(\\w+)/);\n return match ? match[1] : undefined;\n }\n\n /**\n * ์š”์ฒญ ์šฐ์„ ์ˆœ์œ„ ๊ณ„์‚ฐ\n */\n private _calculateRequestPriority(url: string, method: string, graphql?: IGraphQLInfo): 'high' | 'medium' | 'low' {\n // GraphQL mutation์€ ๋†’์€ ์šฐ์„ ์ˆœ์œ„\n if (graphql?.operationType === 'mutation') {\n return 'high';\n }\n\n // POST, PUT, DELETE๋Š” ๋†’์€ ์šฐ์„ ์ˆœ์œ„\n if (['POST', 'PUT', 'DELETE'].includes(method.toUpperCase())) {\n return 'high';\n }\n\n // API ์—”๋“œํฌ์ธํŠธ์ธ์ง€ ํ™•์ธ\n if (url.includes('/api/') || url.includes('/graphql')) {\n return 'medium';\n }\n\n // ์ •์  ๋ฆฌ์†Œ์Šค๋Š” ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„\n if (/\\.(js|css|png|jpg|jpeg|gif|svg|woff|woff2|ttf)(\\?|$)/.test(url)) {\n return 'low';\n }\n\n return 'medium';\n }\n\n /**\n * Fetch ์‘๋‹ต ์ฒ˜๋ฆฌ\n */\n private async _handleFetchResponse(response: Response, request: IEnhancedNetworkRequest): Promise<void> {\n try {\n let responseBody: string | undefined;\n let responseBodySize = 0;\n\n // ์‘๋‹ต ๋ฐ”๋”” ์ฒ˜๋ฆฌ\n if (this._config.captureResponseBody) {\n const responseClone = response.clone();\n const bodyText = await responseClone.text();\n \n responseBodySize = new TextEncoder().encode(bodyText).length;\n responseBody = bodyText.length > this._config.maxBodySize ? \n bodyText.substring(0, this._config.maxBodySize) + '...[truncated]' : \n bodyText;\n }\n\n // ์‘๋‹ต ํ—ค๋” ์ถ”์ถœ\n const responseHeaders = this._extractResponseHeaders(response.headers);\n\n // GraphQL ์‘๋‹ต ํŒŒ์‹ฑ\n const graphqlResponse = request.graphql ? \n this._parseGraphQLResponse(responseBody) : undefined;\n\n const networkResponse: IEnhancedNetworkResponse = {\n id: generateUniqueId(),\n requestId: request.id,\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n body: responseBody,\n timestamp: Date.now(),\n timing: request.timing,\n size: {\n responseHeaders: this._calculateHeadersSize(responseHeaders),\n responseBody: responseBodySize,\n total: this._calculateHeadersSize(responseHeaders) + responseBodySize,\n },\n graphql: graphqlResponse,\n };\n\n this._responseHistory.push(networkResponse);\n\n // ํžˆ์Šคํ† ๋ฆฌ ํฌ๊ธฐ ์ œํ•œ\n if (this._responseHistory.length > this._config.maxHistorySize) {\n this._responseHistory = this._responseHistory.slice(-this._config.maxHistorySize);\n }\n\n // ์ฝœ๋ฐฑ ํ˜ธ์ถœ\n if (this._config.onResponse) {\n try {\n this._config.onResponse(networkResponse);\n } catch (error) {\n console.warn('[NetworkInterceptor] Error in onResponse callback:', error);\n }\n }\n } catch (error) {\n console.warn('[NetworkInterceptor] Error handling fetch response:', error);\n }\n }\n\n /**\n * Fetch ์—๋Ÿฌ ์ฒ˜๋ฆฌ\n */\n private async _handleFetchError(error: Error, request: IEnhancedNetworkRequest): Promise<void> {\n const networkResponse: IEnhancedNetworkResponse = {\n id: generateUniqueId(),\n requestId: request.id,\n status: 0,\n statusText: 'Network Error',\n headers: {},\n body: error.message,\n timestamp: Date.now(),\n timing: request.timing,\n size: {\n responseHeaders: 0,\n responseBody: error.message.length,\n total: error.message.length,\n },\n };\n\n this._responseHistory.push(networkResponse);\n\n // ์—๋Ÿฌ ์ฝœ๋ฐฑ ํ˜ธ์ถœ\n if (this._config.onError) {\n try {\n this._config.onError(error, request);\n } catch (callbackError) {\n console.warn('[NetworkInterceptor] Error in onError callback:', callbackError);\n }\n }\n }\n\n /**\n * XHR ์‘๋‹ต ์ฒ˜๋ฆฌ\n */\n private _handleXHRResponse(xhr: XMLHttpRequest, request: IEnhancedNetworkRequest): void {\n try {\n let responseBody: string | undefined;\n let responseBodySize = 0;\n\n if (this._config.captureResponseBody) {\n responseBodySize = new TextEncoder().encode(xhr.responseText).length;\n responseBody = xhr.responseText.length > this._config.maxBodySize ? \n xhr.responseText.substring(0, this._config.maxBodySize) + '...[truncated]' : \n xhr.responseText;\n }\n\n // ์‘๋‹ต ํ—ค๋” ์ถ”์ถœ (XHR์—์„œ๋Š” ์ œํ•œ์ )\n const responseHeaders: Record<string, string> = {};\n const headerString = xhr.getAllResponseHeaders();\n if (headerString) {\n headerString.split('\\r\\n').forEach(line => {\n const parts = line.split(': ');\n if (parts.length === 2) {\n responseHeaders[parts[0]] = parts[1];\n }\n });\n }\n\n // GraphQL ์‘๋‹ต ํŒŒ์‹ฑ\n const graphqlResponse = request.graphql ? \n this._parseGraphQLResponse(responseBody) : undefined;\n\n const networkResponse: IEnhancedNetworkResponse = {\n id: generateUniqueId(),\n requestId: request.id,\n status: xhr.status,\n statusText: xhr.statusText,\n headers: responseHeaders,\n body: responseBody,\n timestamp: Date.now(),\n timing: request.timing,\n size: {\n responseHeaders: this._calculateHeadersSize(responseHeaders),\n responseBody: responseBodySize,\n total: this._calculateHeadersSize(responseHeaders) + responseBodySize,\n },\n graphql: graphqlResponse,\n };\n\n this._responseHistory.push(networkResponse);\n\n // ์ฝœ๋ฐฑ ํ˜ธ์ถœ\n if (this._config.onResponse) {\n try {\n this._config.onResponse(networkResponse);\n } catch (error) {\n console.warn('[NetworkInterceptor] Error in onResponse callback:', error);\n }\n }\n } catch (error) {\n console.warn('[NetworkInterceptor] Error handling XHR response:', error);\n }\n }\n\n /**\n * GraphQL ์‘๋‹ต ํŒŒ์‹ฑ\n */\n private _parseGraphQLResponse(body?: string): { errors?: unknown[]; data?: unknown; extensions?: Record<string, unknown> } | undefined {\n if (!body) return undefined;\n\n try {\n const parsed = JSON.parse(body);\n if (parsed.data !== undefined || parsed.errors !== undefined) {\n return {\n errors: parsed.errors,\n data: parsed.data,\n extensions: parsed.extensions,\n };\n }\n } catch (error) {\n // GraphQL ์‘๋‹ต์ด ์•„๋‹˜\n }\n\n return undefined;\n }\n\n /**\n * ํ—ค๋” ํฌ๊ธฐ ๊ณ„์‚ฐ\n */\n private _calculateHeadersSize(headers: Record<string, string>): number {\n return Object.entries(headers).reduce((size, [key, value]) => {\n return size + key.length + value.length + 4; // \": \" + \"\\r\\n\"\n }, 0);\n }\n\n /**\n * ์š”์ฒญ ํ—ค๋” ์ถ”์ถœ\n */\n private _extractHeaders(headers?: HeadersInit): Record<string, string> {\n const result: Record<string, string> = {};\n\n if (!headers) {\n return result;\n }\n\n if (headers instanceof Headers) {\n headers.forEach((value, key) => {\n result[key] = value;\n });\n } else if (Array.isArray(headers)) {\n headers.forEach(([key, value]) => {\n result[key] = value;\n });\n } else {\n Object.entries(headers).forEach(([key, value]) => {\n result[key] = value;\n });\n }\n\n return result;\n }\n\n /**\n * ์‘๋‹ต ํ—ค๋” ์ถ”์ถœ\n */\n private _extractResponseHeaders(headers: Headers): Record<string, string> {\n const result: Record<string, string> = {};\n headers.forEach((value, key) => {\n result[key] = value;\n });\n return result;\n }\n\n /**\n * ํ˜„์žฌ ์ƒํƒœ ๊ฐ€์ ธ์˜ค๊ธฐ\n */\n public getState() {\n return {\n isIntercepting: this._isIntercepting,\n requestCount: this._requestHistory.length,\n responseCount: this._responseHistory.length,\n config: { ...this._config },\n metrics: this.getNetworkMetrics(),\n };\n }\n\n /**\n * ์ •๋ฆฌ\n */\n public destroy(): void {\n this.stopIntercepting();\n this.clearHistory();\n }\n}\n","import { IEnhancedNetworkRequest, IEnhancedNetworkResponse, INetworkTiming } from './types';\nimport { generateUniqueId } from '../utils';\n\n/**\n * Axios ์ธํ„ฐ์…‰ํ„ฐ - axios ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€์˜ ํ†ตํ•ฉ\n */\nexport class AxiosInterceptor {\n private _isIntercepting: boolean = false;\n private _requestInterceptorId: number | null = null;\n private _responseInterceptorId: number | null = null;\n private _onRequest?: (request: IEnhancedNetworkRequest) => void;\n private _onResponse?: (response: IEnhancedNetworkResponse) => void;\n private _onError?: (error: Error, request: IEnhancedNetworkRequest) => void;\n\n constructor(callbacks?: {\n onRequest?: (request: IEnhancedNetworkRequest) => void;\n onResponse?: (response: IEnhancedNetworkResponse) => void;\n onError?: (error: Error, request: IEnhancedNetworkRequest) => void;\n }) {\n this._onRequest = callbacks?.onRequest;\n this._onResponse = callbacks?.onResponse;\n this._onError = callbacks?.onError;\n }\n\n /**\n * Axios ์ธํ„ฐ์…‰์…˜ ์‹œ์ž‘\n */\n public startIntercepting(): void {\n if (this._isIntercepting) {\n console.warn('[AxiosInterceptor] Already intercepting');\n return;\n }\n\n // axios๊ฐ€ ์ „์—ญ์— ์žˆ๋Š”์ง€ ํ™•์ธ\n const axios = this._getAxiosInstance();\n if (!axios) {\n console.warn('[AxiosInterceptor] Axios not found in global scope');\n return;\n }\n\n console.log('[AxiosInterceptor] Starting axios interception...');\n\n try {\n // ์š”์ฒญ ์ธํ„ฐ์…‰ํ„ฐ ๋“ฑ๋ก\n this._requestInterceptorId = axios.interceptors.request.use(\n (config: any) => this._handleAxiosRequest(config),\n (error: any) => this._handleAxiosRequestError(error)\n );\n\n // ์‘๋‹ต ์ธํ„ฐ์…‰ํ„ฐ ๋“ฑ๋ก\n this._responseInterceptorId = axios.interceptors.response.use(\n (response: any) => this._handleAxiosResponse(response),\n (error: any) => this._handleAxiosResponseError(error)\n );\n\n this._isIntercepting = true;\n console.log('[AxiosInterceptor] Axios interception started');\n } catch (error) {\n console.error('[AxiosInterceptor] Failed to start interception:', error);\n }\n }\n\n /**\n * Axios ์ธํ„ฐ์…‰์…˜ ์ค‘์ง€\n */\n public stopIntercepting(): void {\n if (!this._isIntercepting) {\n return;\n }\n\n const axios = this._getAxiosInstance();\n if (!axios) {\n return;\n }\n\n console.log('[AxiosInterceptor] Stopping axios interception...');\n\n try {\n // ์ธํ„ฐ์…‰ํ„ฐ ์ œ๊ฑฐ\n if (this._requestInterceptorId !== null) {\n axios.interceptors.request.eject(this._requestInterceptorId);\n this._requestInterceptorId = null;\n }\n\n if (this._responseInterceptorId !== null) {\n axios.interceptors.response.eject(this._responseInterceptorId);\n this._responseInterceptorId = null;\n }\n\n this._isIntercepting = false;\n console.log('[AxiosInterceptor] Axios interception stopped');\n } catch (error) {\n console.error('[AxiosInterceptor] Failed to stop interception:', error);\n }\n }\n\n /**\n * ์ „์—ญ axios ์ธ์Šคํ„ด์Šค ํƒ์ง€\n */\n private _getAxiosInstance(): any {\n // 1. ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ์—์„œ window.axios ํ™•์ธ\n if (typeof window !== 'undefined' && (window as any).axios) {\n return (window as any).axios;\n }\n\n // 2. Node.js ํ™˜๊ฒฝ์—์„œ global.axios ํ™•์ธ\n if (typeof global !== 'undefined' && (global as any).axios) {\n return (global as any).axios;\n }\n\n // 3. CommonJS require ์‹œ๋„ (๋™์ ์œผ๋กœ, ํ•˜์ง€๋งŒ ์•ˆ์ „ํ•˜๊ฒŒ)\n try {\n const moduleRequire = typeof require !== 'undefined' ? require : null;\n if (moduleRequire) {\n return moduleRequire('axios');\n }\n } catch (error) {\n // axios๊ฐ€ ์„ค์น˜๋˜์ง€ ์•Š์Œ ๋˜๋Š” require ์‹คํŒจ\n }\n\n // 4. ES Module์ด ์ด๋ฏธ ๋กœ๋“œ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ\n if (typeof window !== 'undefined' && (window as any).__AXIOS_INSTANCE__) {\n return (window as any).__AXIOS_INSTANCE__;\n }\n\n return null;\n }\n\n /**\n * Axios ์š”์ฒญ ์ธํ„ฐ์…‰ํ„ฐ\n */\n private _handleAxiosRequest(config: any): any {\n try {\n const requestId = generateUniqueId();\n const startTime = performance.now();\n\n // Axios config์—์„œ ์š”์ฒญ ์ •๋ณด ์ถ”์ถœ\n const url = this._buildFullUrl(config);\n const method = (config.method || 'GET').toUpperCase();\n const headers = config.headers || {};\n\n // ์š”์ฒญ ๋ฐ”๋”” ์ฒ˜๋ฆฌ\n let requestBody: string | undefined;\n let requestBodySize = 0;\n\n if (config.data) {\n if (typeof config.data === 'string') {\n requestBody = config.data;\n requestBodySize = new TextEncoder().encode(config.data).length;\n } else if (config.data instanceof FormData) {\n requestBody = '[FormData]';\n // FormData ํฌ๊ธฐ๋Š” ์ •ํ™•ํžˆ ์ธก์ •ํ•˜๊ธฐ ์–ด๋ ค์›€\n requestBodySize = 0;\n } else {\n try {\n requestBody = JSON.stringify(config.data);\n requestBodySize = new TextEncoder().encode(requestBody).length;\n } catch (error) {\n requestBody = '[Unserializable Data]';\n requestBodySize = 0;\n }\n }\n }\n\n // ํƒ€์ด๋ฐ ์ •๋ณด\n const timing: INetworkTiming = {\n startTime,\n requestStart: startTime,\n responseStart: 0,\n responseEnd: 0,\n duration: 0,\n };\n\n // ๊ฐ•ํ™”๋œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๊ฐ์ฒด ์ƒ์„ฑ\n const enhancedRequest: IEnhancedNetworkRequest = {\n id: requestId,\n url,\n method,\n headers: this._normalizeHeaders(headers),\n body: requestBody,\n timestamp: Date.now(),\n timing,\n size: {\n requestHeaders: this._calculateHeadersSize(headers),\n requestBody: requestBodySize,\n total: this._calculateHeadersSize(headers) + requestBodySize,\n },\n graphql: this._detectGraphQL(headers, requestBody),\n priority: this._calculatePriority(url, method),\n };\n\n // config์— ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ (์‘๋‹ต์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด)\n config._debugTimeMachine = {\n requestId,\n startTime,\n enhancedRequest,\n };\n\n // ์ฝœ๋ฐฑ ํ˜ธ์ถœ\n if (this._onRequest) {\n try {\n this._onRequest(enhancedRequest);\n } catch (error) {\n console.warn('[AxiosInterceptor] Error in onRequest callback:', error);\n }\n }\n\n return config;\n } catch (error) {\n console.error('[AxiosInterceptor] Error in request interceptor:', error);\n return config;\n }\n }\n\n /**\n * Axios ์š”์ฒญ ์—๋Ÿฌ ์ธํ„ฐ์…‰ํ„ฐ\n */\n private _handleAxiosRequestError(error: any): Promise<any> {\n console.error('[AxiosInterceptor] Request error:', error);\n return Promise.reject(error);\n }\n\n /**\n * Axios ์‘๋‹ต ์ธํ„ฐ์…‰ํ„ฐ\n */\n private _handleAxiosResponse(response: any): any {\n try {\n const config = response.config;\n const metadata = config._debugTimeMachine;\n\n if (!metadata) {\n // ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ์ถ”์  ๋ถˆ๊ฐ€\n return response;\n }\n\n const responseEndTime = performance.now();\n \n // ํƒ€์ด๋ฐ ์—…๋ฐ์ดํŠธ\n metadata.enhancedRequest.timing.responseStart = responseEndTime - 10; // ๊ทผ์‚ฌ์น˜\n metadata.enhancedRequest.timing.responseEnd = responseEndTime;\n metadata.enhancedRequest.timing.duration = responseEndTime - metadata.startTime;\n\n // ์‘๋‹ต ๋ฐ”๋”” ์ฒ˜๋ฆฌ\n let responseBody: string | undefined;\n let responseBodySize = 0;\n\n if (response.data) {\n if (typeof response.data === 'string') {\n responseBody = response.data;\n responseBodySize = new TextEncoder().encode(response.data).length;\n } else {\n try {\n responseBody = JSON.stringify(response.data);\n responseBodySize = new TextEncoder().encode(responseBody).length;\n } catch (error) {\n responseBody = '[Unserializable Data]';\n responseBodySize = 0;\n }\n }\n }\n\n // ์‘๋‹ต ํ—ค๋” ์ฒ˜๋ฆฌ\n const responseHeaders = this._normalizeHeaders(response.headers || {});\n\n // ๊ฐ•ํ™”๋œ ๋„คํŠธ์›Œํฌ ์‘๋‹ต ๊ฐ์ฒด ์ƒ์„ฑ\n const enhancedResponse: IEnhancedNetworkResponse = {\n id: generateUniqueId(),\n requestId: metadata.requestId,\n status: response.status,\n statusText: response.statusText || '',\n headers: responseHeaders,\n body: responseBody,\n timestamp: Date.now(),\n timing: metadata.enhancedRequest.timing,\n size: {\n responseHeaders: this._calculateHeadersSize(responseHeaders),\n responseBody: responseBodySize,\n total: this._calculateHeadersSize(responseHeaders) + responseBodySize,\n },\n graphql: metadata.enhancedRequest.graphql ? \n this._parseGraphQLResponse(responseBody) : undefined,\n };\n\n // ์ฝœ๋ฐฑ ํ˜ธ์ถœ\n if (this._onResponse) {\n try {\n this._onResponse(enhancedResponse);\n } catch (error) {\n console.warn('[AxiosInterceptor] Error in onResponse callback:', error);\n }\n }\n\n return response;\n } catch (error) {\n console.error('[AxiosInterceptor] Error in response interceptor:', error);\n return response;\n }\n }\n\n /**\n * Axios ์‘๋‹ต ์—๋Ÿฌ ์ธํ„ฐ์…‰ํ„ฐ\n */\n private _handleAxiosResponseError(error: any): Promise<any> {\n try {\n const config = error.config;\n const metadata = config?._debugTimeMachine;\n\n if (metadata) {\n const responseEndTime = performance.now();\n \n // ํƒ€์ด๋ฐ ์—…๋ฐ์ดํŠธ\n metadata.enhancedRequest.timing.responseEnd = responseEndTime;\n metadata.enhancedRequest.timing.duration = responseEndTime - metadata.startTime;\n\n // ์—๋Ÿฌ ์‘๋‹ต ์ƒ์„ฑ\n const enhancedResponse: IEnhancedNetworkResponse = {\n id: generateUniqueId(),\n requestId: metadata.requestId,\n status: error.response?.status || 0,\n statusText: error.response?.statusText || error.message,\n headers: this._normalizeHeaders(error.response?.headers || {}),\n body: error.message,\n timestamp: Date.now(),\n timing: metadata.enhancedRequest.timing,\n size: {\n responseHeaders: this._calculateHeadersSize(error.response?.headers || {}),\n responseBody: error.message.length,\n total: this._calculateHeadersSize(error.response?.headers || {}) + error.message.length,\n },\n };\n\n // ์—๋Ÿฌ ์ฝœ๋ฐฑ ํ˜ธ์ถœ\n if (this._onError) {\n try {\n this._onError(error, metadata.enhancedRequest);\n } catch (callbackError) {\n console.warn('[AxiosInterceptor] Error in onError callback:', callbackError);\n }\n }\n\n // ์‘๋‹ต ์ฝœ๋ฐฑ๋„ ํ˜ธ์ถœ (์—๋Ÿฌ ์‘๋‹ต๋„ ์‘๋‹ต์ด๋ฏ€๋กœ)\n if (this._onResponse) {\n try {\n this._onResponse(enhancedResponse);\n } catch (callbackError) {\n console.warn('[AxiosInterceptor] Error in onResponse callback:', callbackError);\n }\n }\n }\n } catch (interceptorError) {\n console.error('[AxiosInterceptor] Error in response error interceptor:', interceptorError);\n }\n\n return Promise.reject(error);\n }\n\n /**\n * ์ „์ฒด URL ๊ตฌ์„ฑ\n */\n private _buildFullUrl(config: any): string {\n const baseURL = config.baseURL || '';\n const url = config.url || '';\n \n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n \n return baseURL + url;\n }\n\n /**\n * ํ—ค๋” ์ •๊ทœํ™”\n */\n private _normalizeHeaders(headers: any): Record<string, string> {\n const normalized: Record<string, string> = {};\n \n if (!headers) {\n return normalized;\n }\n\n // axios headers๋Š” ๋‹ค์–‘ํ•œ ํ˜•ํƒœ์ผ ์ˆ˜ ์žˆ์Œ\n Object.entries(headers).forEach(([key, value]) => {\n if (typeof value === 'string') {\n normalized[key] = value;\n } else if (value != null) {\n normalized[key] = String(value);\n }\n });\n\n return normalized;\n }\n\n /**\n * ํ—ค๋” ํฌ๊ธฐ ๊ณ„์‚ฐ\n */\n private _calculateHeadersSize(headers: any): number {\n const normalized = this._normalizeHeaders(headers);\n return Object.entries(normalized).reduce((size, [key, value]) => {\n return size + key.length + value.length + 4; // \": \" + \"\\r\\n\"\n }, 0);\n }\n\n /**\n * GraphQL ๊ฐ์ง€\n */\n private _detectGraphQL(headers: any, body?: string): any {\n const normalized = this._normalizeHeaders(headers);\n const contentType = normalized['content-type'] || normalized['Content-Type'] || '';\n \n if (contentType.includes('application/graphql')) {\n return {\n operationType: this._detectGraphQLOperationType(body || ''),\n operationName: this._extractGraphQLOperationName(body || ''),\n };\n }\n\n if (contentType.includes('application/json') && body) {\n try {\n const parsed = JSON.parse(body);\n if (parsed.query || parsed.operationName) {\n return {\n operationType: this._detectGraphQLOperationType(parsed.query),\n operationName: parsed.operationName,\n variables: parsed.variables,\n extensions: parsed.extensions,\n };\n }\n } catch (error) {\n // JSON ํŒŒ์‹ฑ ์‹คํŒจ\n }\n }\n\n return undefined;\n }\n\n /**\n * GraphQL ์˜คํผ๋ ˆ์ด์…˜ ํƒ€์ž… ๊ฐ์ง€\n */\n private _detectGraphQLOperationType(query: string): 'query' | 'mutation' | 'subscription' {\n if (!query) return 'query';\n \n const trimmed = query.trim().toLowerCase();\n if (trimmed.startsWith('mutation')) return 'mutation';\n if (trimmed.startsWith('subscription')) return 'subscription';\n return 'query';\n }\n\n /**\n * GraphQL ์˜คํผ๋ ˆ์ด์…˜ ์ด๋ฆ„ ์ถ”์ถœ\n */\n private _extractGraphQLOperationName(query: string): string | undefined {\n if (!query) return undefined;\n \n const match = query.match(/(?:query|mutation|subscription)\\s+(\\w+)/);\n return match ? match[1] : undefined;\n }\n\n /**\n * GraphQL ์‘๋‹ต ํŒŒ์‹ฑ\n */\n private _parseGraphQLResponse(body?: string): any {\n if (!body) return undefined;\n\n try {\n const parsed = JSON.parse(body);\n if (parsed.data !== undefined || parsed.errors !== undefined) {\n return {\n errors: parsed.errors,\n data: parsed.data,\n extensions: parsed.extensions,\n };\n }\n } catch (error) {\n // GraphQL ์‘๋‹ต์ด ์•„๋‹˜\n }\n\n return undefined;\n }\n\n /**\n * ์š”์ฒญ ์šฐ์„ ์ˆœ์œ„ ๊ณ„์‚ฐ\n */\n private _calculatePriority(url: string, method: string): 'high' | 'medium' | 'low' {\n if (['POST', 'PUT', 'DELETE'].includes(method.toUpperCase())) {\n return 'high';\n }\n\n if (url.includes('/api/') || url.includes('/graphql')) {\n return 'medium';\n }\n\n return 'low';\n }\n\n /**\n * ํ˜„์žฌ ์ƒํƒœ ํ™•์ธ\n */\n public get isIntercepting(): boolean {\n return this._isIntercepting;\n }\n\n /**\n * ์ƒํƒœ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ\n */\n public getState() {\n return {\n isIntercepting: this._isIntercepting,\n hasRequestInterceptor: this._requestInterceptorId !== null,\n hasResponseInterceptor: this._responseInterceptorId !== null,\n axiosAvailable: this._getAxiosInstance() !== null,\n };\n }\n\n /**\n * ์ •๋ฆฌ\n */\n public destroy(): void {\n this.stopIntercepting();\n }\n}\n","import { IEnhancedNetworkRequest, IEnhancedNetworkResponse } from './types';\n\nexport interface IWebSocketConnectorMessage {\n type: 'network-request' | 'network-response' | 'error' | 'ping' | 'pong';\n data: unknown;\n timestamp: number;\n sessionId: string;\n}\n\nexport interface IWebSocketConnectorConfig {\n url: string;\n reconnectInterval?: number;\n maxReconnectAttempts?: number;\n enableHeartbeat?: boolean;\n heartbeatInterval?: number;\n sessionId?: string;\n}\n\n/**\n * WebSocket ์ปค๋„ฅํ„ฐ - Debug Time Machine ์„œ๋ฒ„์™€ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ \n */\nexport class WebSocketConnector {\n private _config: Required<IWebSocketConnectorConfig>;\n private _ws: WebSocket | null = null;\n private _isConnected: boolean = false;\n private _reconnectAttempts: number = 0;\n private _heartbeatTimer: NodeJS.Timeout | null = null;\n private _reconnectTimer: NodeJS.Timeout | null = null;\n private _messageQueue: IWebSocketConnectorMessage[] = [];\n private _sessionId: string;\n\n constructor(config: IWebSocketConnectorConfig) {\n this._config = {\n reconnectInterval: 3000,\n maxReconnectAttempts: 10,\n enableHeartbeat: true,\n heartbeatInterval: 30000,\n sessionId: this._generateSessionId(),\n ...config,\n };\n\n this._sessionId = this._config.sessionId;\n console.log(`[WebSocketConnector] Initialized with session: ${this._sessionId}`);\n }\n\n /**\n * WebSocket ์—ฐ๊ฒฐ ์‹œ์ž‘\n */\n public async connect(): Promise<void> {\n if (this._isConnected && this._ws) {\n console.log('[WebSocketConnector] Already connected');\n return;\n }\n\n console.log(`[WebSocketConnector] Connecting to ${this._config.url}...`);\n\n try {\n this._ws = new WebSocket(this._config.url);\n \n this._ws.onopen = this._onOpen.bind(this);\n this._ws.onclose = this._onClose.bind(this);\n this._ws.onerror = this._onError.bind(this);\n this._ws.onmessage = this._onMessage.bind(this);\n\n // ์—ฐ๊ฒฐ ์™„๋ฃŒ๋ฅผ ๊ธฐ๋‹ค๋ฆผ\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error('WebSocket connection timeout'));\n }, 10000);\n\n this._ws!.addEventListener('open', () => {\n clearTimeout(timeout);\n resolve();\n });\n\n this._ws!.addEventListener('error', (error) => {\n clearTimeout(timeout);\n reject(error);\n });\n });\n } catch (error) {\n console.error('[WebSocketConnector] Connection failed:', error);\n throw error;\n }\n }\n\n /**\n * WebSocket ์—ฐ๊ฒฐ ํ•ด์ œ\n */\n public disconnect(): void {\n console.log('[WebSocketConnector] Disconnecting...');\n \n this._clearTimers();\n \n if (this._ws) {\n this._ws.onopen = null;\n this._ws.onclose = null;\n this._ws.onerror = null;\n this._ws.onmessage = null;\n \n if (this._ws.readyState === WebSocket.OPEN) {\n this._ws.close(1000, 'Manual disconnect');\n }\n \n this._ws = null;\n }\n\n this._isConnected = false;\n this._reconnectAttempts = 0;\n \n console.log('[WebSocketConnector] Disconnected');\n }\n\n /**\n * ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์ „์†ก\n */\n public sendNetworkRequest(request: IEnhancedNetworkRequest): void {\n const message: IWebSocketConnectorMessage = {\n type: 'network-request',\n data: request,\n timestamp: Date.now(),\n sessionId: this._sessionId,\n };\n\n this._sendMessage(message);\n }\n\n /**\n * ๋„คํŠธ์›Œํฌ ์‘๋‹ต ์ „์†ก\n */\n public sendNetworkResponse(response: IEnhancedNetworkResponse): void {\n const message: IWebSocketConnectorMessage = {\n type: 'network-response',\n data: response,\n timestamp: Date.now(),\n sessionId: this._sessionId,\n };\n\n this._sendMessage(message);\n }\n\n /**\n * ์—๋Ÿฌ ์ „์†ก\n */\n public sendError(error: Error, request?: IEnhancedNetworkRequest): void {\n const message: IWebSocketConnectorMessage = {\n type: 'error',\n data: {\n message: error.message,\n stack: error.stack,\n request: request,\n },\n timestamp: Date.now(),\n sessionId: this._sessionId,\n };\n\n this._sendMessage(message);\n }\n\n /**\n * ์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ\n */\n public get isConnected(): boolean {\n return this._isConnected && this._ws?.readyState === WebSocket.OPEN;\n }\n\n /**\n * ์„ธ์…˜ ID ๊ฐ€์ ธ์˜ค๊ธฐ\n */\n public get sessionId(): string {\n return this._sessionId;\n }\n\n /**\n * ๋Œ€๊ธฐ ์ค‘์ธ ๋ฉ”์‹œ์ง€ ์ˆ˜\n */\n public get queueSize(): number {\n return this._messageQueue.length;\n }\n\n /**\n * WebSocket ์—ด๋ฆผ ์ด๋ฒคํŠธ\n */\n private _onOpen(event: Event): void {\n console.log('[WebSocketConnector] Connected successfully');\n \n this._isConnected = true;\n this._reconnectAttempts = 0;\n \n // ๋Œ€๊ธฐ ์ค‘์ธ ๋ฉ”์‹œ์ง€๋“ค ์ „์†ก\n this._flushMessageQueue();\n \n // ํ•˜ํŠธ๋น„ํŠธ ์‹œ์ž‘\n if (this._config.enableHeartbeat) {\n this._startHeartbeat();\n }\n }\n\n /**\n * WebSocket ๋‹ซํž˜ ์ด๋ฒคํŠธ\n */\n private _onClose(event: CloseEvent): void {\n console.log(`[WebSocketConnector] Connection closed: ${event.code} ${event.reason}`);\n \n this._isConnected = false;\n this._clearTimers();\n \n // ๋น„์ •์ƒ์ ์ธ ์ข…๋ฃŒ์ธ ๊ฒฝ์šฐ ์žฌ์—ฐ๊ฒฐ ์‹œ๋„\n if (event.code !== 1000 && this._reconnectAttempts < this._config.maxReconnectAttempts) {\n this._scheduleReconnect();\n }\n }\n\n /**\n * WebSocket ์—๋Ÿฌ ์ด๋ฒคํŠธ\n */\n private _onError(event: Event): void {\n console.error('[WebSocketConnector] WebSocket error:', event);\n \n if (!this._isConnected && this._reconnectAttempts < this._config.maxReconnectAttempts) {\n this._scheduleReconnect();\n }\n }\n\n /**\n * WebSocket ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ \n */\n private _onMessage(event: MessageEvent): void {\n try {\n const message = JSON.parse(event.data) as IWebSocketConnectorMessage;\n \n if (message.type === 'ping') {\n // Ping์— Pong์œผ๋กœ ์‘๋‹ต\n this._sendMessage({\n type: 'pong',\n data: message.data,\n timestamp: Date.now(),\n sessionId: this._sessionId,\n });\n }\n // ๋‹ค๋ฅธ ๋ฉ”์‹œ์ง€ ํƒ€์ž…๋“ค์€ ํ•„์š”์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ\n } catch (error) {\n console.warn('[WebSocketConnector] Failed to parse message:', error);\n }\n }\n\n /**\n * ๋ฉ”์‹œ์ง€ ์ „์†ก\n */\n private _sendMessage(message: IWebSocketConnectorMessage): void {\n if (!this.isConnected) {\n // ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ํ์— ์ €์žฅ\n this._messageQueue.push(message);\n \n // ํ ํฌ๊ธฐ ์ œํ•œ (๋ฉ”๋ชจ๋ฆฌ ๋ณดํ˜ธ)\n if (this._messageQueue.length > 1000) {\n this._messageQueue = this._messageQueue.slice(-1000);\n }\n \n // ์ž๋™ ์—ฐ๊ฒฐ ์‹œ๋„\n if (!this._isConnected && this._reconnectAttempts === 0) {\n this._scheduleReconnect();\n }\n \n return;\n }\n\n try {\n const serialized = JSON.stringify(message);\n this._ws!.send(serialized);\n } catch (error) {\n console.error('[WebSocketConnector] Failed to send message:', error);\n // ์ „์†ก ์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ํ์— ๋‹ค์‹œ ์ถ”๊ฐ€\n this._messageQueue.unshift(message);\n }\n }\n\n /**\n * ๋Œ€๊ธฐ ์ค‘์ธ ๋ฉ”์‹œ์ง€๋“ค ์ „์†ก\n */\n private _flushMessageQueue(): void {\n console.log(`[WebSocketConnector] Flushing ${this._messageQueue.length} queued messages`);\n \n const messages = [...this._messageQueue];\n this._messageQueue = [];\n \n messages.forEach(message => {\n this._sendMessage(message);\n });\n }\n\n /**\n * ์žฌ์—ฐ๊ฒฐ ์Šค์ผ€์ค„๋ง\n */\n private _scheduleReconnect(): void {\n if (this._reconnectTimer) {\n return; // ์ด๋ฏธ ์žฌ์—ฐ๊ฒฐ์ด ์Šค์ผ€์ค„๋จ\n }\n\n this._reconnectAttempts++;\n const delay = this._config.reconnectInterval * Math.pow(1.5, this._reconnectAttempts - 1);\n \n console.log(`[WebSocketConnector] Scheduling reconnect attempt ${this._reconnectAttempts}/${this._config.maxReconnectAttempts} in ${delay}ms`);\n \n this._reconnectTimer = setTimeout(async () => {\n this._reconnectTimer = null;\n \n try {\n await this.connect();\n } catch (error) {\n console.error('[WebSocketConnector] Reconnect failed:', error);\n \n if (this._reconnectAttempts < this._config.maxReconnectAttempts) {\n this._scheduleReconnect();\n } else {\n console.error('[WebSocketConnector] Max reconnect attempts reached');\n }\n }\n }, delay);\n }\n\n /**\n * ํ•˜ํŠธ๋น„ํŠธ ์‹œ์ž‘\n */\n private _startHeartbeat(): void {\n this._heartbeatTimer = setInterval(() => {\n if (this.isConnected) {\n this._sendMessage({\n type: 'ping',\n data: { sessionId: this._sessionId },\n timestamp: Date.now(),\n sessionId: this._sessionId,\n });\n }\n }, this._config.heartbeatInterval);\n }\n\n /**\n * ํƒ€์ด๋จธ๋“ค ์ •๋ฆฌ\n */\n private _clearTimers(): void {\n if (this._heartbeatTimer) {\n clearInterval(this._heartbeatTimer);\n this._heartbeatTimer = null;\n }\n \n if (this._reconnectTimer) {\n clearTimeout(this._reconnectTimer);\n this._reconnectTimer = null;\n }\n }\n\n /**\n * ์„ธ์…˜ ID ์ƒ์„ฑ\n */\n private _generateSessionId(): string {\n return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n }\n\n /**\n * ์ƒํƒœ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ\n */\n public getState() {\n return {\n isConnected: this._isConnected,\n sessionId: this._sessionId,\n queueSize: this._messageQueue.length,\n reconnectAttempts: this._reconnectAttempts,\n websocketState: this._ws?.readyState,\n };\n }\n\n /**\n * ์ •๋ฆฌ\n */\n public destroy(): void {\n this.disconnect();\n this._messageQueue = [];\n }\n}\n","import { NetworkInterceptor } from './NetworkInterceptor';\nimport { AxiosInterceptor } from './AxiosInterceptor';\nimport { WebSocketConnector } from './WebSocketConnector';\nimport { IEnhancedNetworkRequest, IEnhancedNetworkResponse, INetworkInterceptorConfig } from './types';\n\nexport interface IRealTimeAPIInterceptorConfig extends Partial<INetworkInterceptorConfig> {\n websocket?