let instance: HttpUtils | null = null; export class HttpUtils { /** * 싱글턴 인스턴스를 생성합니다. */ public constructor() { if (instance) { return instance; } instance = this; return instance; } /** * URI 디코딩 후 소문자로 변환하는 헬퍼 함수 * @param value - 디코딩하고 소문자로 변환할 문자열 * @returns 디코딩 및 소문자 변환된 문자열 */ decodeAndLower(value: string): string { try { return decodeURIComponent(value).toLowerCase(); } catch { return value.toLowerCase(); } } /** * 헤더 객체의 키와 값을 전부 소문자로 변환합니다. * @param headers - Record 형태의 헤더 맵 * @returns - 키와 값이 모두 소문자로 변환된 새 헤더 맵 */ lowerCaseAllHeaders( headers: Record ): Record { const result: Record = {}; for (const [rawKey, rawValue] of Object.entries(headers)) { const key = this.decodeAndLower(rawKey); if (Array.isArray(rawValue)) { result[key] = rawValue.map((v) => this.decodeAndLower(v)); } else { result[key] = this.decodeAndLower(rawValue); } } return result; } getQueryParamFromURI(uri: string, key: string): string | null { uri = uri.toLowerCase(); key = key.toLowerCase(); try { const urlObj = new URL(uri); return urlObj.searchParams.get(key); } catch (e) { return null; } } // Query /** * 주어진 쿼리 문자열(query)에서 key에 해당하는 값을 반환합니다. * @param query - "a=1&b=2..." 형태의 쿼리 문자열 (맨 앞 ? 는 없어야 합니다) * @param key - 가져오고 싶은 파라미터 이름 * @returns - 해당 파라미터 값, 없으면 null */ getQueryParam(query: string, key: string): string | null { query = query.toLowerCase(); key = key.toLowerCase(); const params = new URLSearchParams(query); return params.get(key); } /** * 주어진 쿼리 문자열(query)에 key=value를 설정하고, 전체 쿼리 문자열을 반환합니다. * - 이미 key가 있으면 덮어쓰기(set), 없으면 새로 추가합니다. * @param query - "a=1&b=2..." 형태의 쿼리 문자열 (맨 앞 ? 는 없어야 합니다) * @param key - 설정할 파라미터 이름 * @param value - 설정할 값 * @returns - "a=1&b=2&c=3..." 형태의 새로운 쿼리 문자열 */ setQueryParam(query: string, key: string, value: string): string { query = query.toLowerCase(); key = key.toLowerCase(); value = value.toLowerCase(); const params = new URLSearchParams(query); params.set(key, value); return params.toString(); } /** * 주어진 쿼리 문자열(query)에서 key에 해당하는 파라미터를 삭제(delete)하고, * 전체 쿼리 문자열을 반환합니다. * @param query - "a=1&b=2..." 형태의 쿼리 문자열 (맨 앞 ? 는 없어야 합니다) * @param key - 삭제할 파라미터 이름 * @returns - 삭제된 상태의 새로운 쿼리 문자열 */ removeQueryParam(query: string, key: string): string { query = query.toLowerCase(); key = key.toLowerCase(); const params = new URLSearchParams(query); params.delete(key); return params.toString(); } // Headers /** * 주어진 헤더 맵에서 name에 해당하는 첫 번째 헤더 값을 반환합니다. * @param headers - Response.getHeaders() 가 반환하는 객체 * @param name - 꺼내고 싶은 헤더 이름 (예: "location", "Content-Type") * @returns - 해당 헤더의 첫 번째 값, 없으면 null */ getHeaderValue( headers: Record, name: string ): string | null { const normalized = this.lowerCaseAllHeaders(headers); const target = name.toLowerCase(); for (const [key, value] of Object.entries(normalized)) { if (key === target) { let rawValue: string | null = null; if (Array.isArray(value)) { rawValue = value.length > 0 && value[0] ? value[0] : null; } else { rawValue = value.length > 0 ? value : null; } if (rawValue !== null) { try { return decodeURIComponent(rawValue); } catch { return rawValue; } } } } return null; } /** * 주어진 헤더 맵에서 name에 해당하는 헤더 값을 value로 변경한 새 맵을 반환합니다. * - 기존 헤더 이름의 대소문자를 보존합니다. * - value가 string인 경우 [value] 형태로, string[]인 경우 그대로 사용합니다. * - 기존에 해당 헤더가 없으면 새로 추가합니다. * * @param headers - 키가 헤더 이름, 값이 문자열 배열인 헤더 맵 * @param name - 변경할 헤더 이름 (예: "Authorization", "X-Custom-Header") * @param value - 새로 설정할 값 (string 또는 string[]) * @returns - 지정된 헤더가 업데이트된 새로운 헤더 맵 */ setHeaderValue( headers: Record, name: string, value: string | string[] ): Record { headers = this.lowerCaseAllHeaders(headers); const lowerName = name.toLowerCase(); const newHeaders: Record = {}; // 1) 기존 헤더 복사하되, name과 일치하는 항목은 value로 대체 for (const [key, vals] of Object.entries(headers)) { if (key.toLowerCase() === lowerName) { newHeaders[key] = Array.isArray(value) ? value : [value]; } else { newHeaders[key] = Array.isArray(vals) ? vals : [vals]; } } // 2) 해당 헤더가 원래 없었다면 새로 추가 const exists = Object.keys(newHeaders).some( (k) => k.toLowerCase() === lowerName ); if (!exists) { newHeaders[name] = Array.isArray(value) ? value : [value]; } return newHeaders; } /** * 주어진 헤더 맵에서 특정 이름(들)에 해당하는 헤더를 제거한 새 맵을 반환합니다. * @param headers - 키가 헤더 이름, 값이 문자열 배열인 헤더 맵 * @param namesToRemove - 제거할 헤더 이름(하나 혹은 배열). 대소문자 구분 없이 매칭됩니다. * @returns - 지정된 헤더가 제외된 새로운 헤더 맵 */ removeHeaders( headers: Record, namesToRemove: string | string[] ): Record { headers = this.lowerCaseAllHeaders(headers); const toRemove = Array.isArray(namesToRemove) ? namesToRemove.map((n) => n.toLowerCase()) : [namesToRemove.toLowerCase()]; const filtered: Record = {}; for (const [key, vals] of Object.entries(headers)) { if (!toRemove.includes(key.toLowerCase())) { filtered[key] = Array.isArray(vals) ? vals : [vals]; } } return filtered; } }