/** * 잘못된 인코딩으로 깨진 한글 문자열 복구 (레포트·전략명 등) * — UTF-8 바이트가 Latin-1 으로 해석된 경우(3ë ¶ → 3분, 1시간 → 1시간) */ /** Latin-1 바이트 → UTF-8 재해석 */ function repairFromLatin1Bytes(text: string): string | null { try { const bytes = Uint8Array.from(text, c => c.charCodeAt(0) & 0xff); const repaired = new TextDecoder('utf-8', { fatal: false }).decode(bytes); if (!repaired || repaired.includes('\uFFFD')) return null; if (/[\uAC00-\uD7A3]/.test(repaired)) return repaired; } catch { /* ignore */ } return null; } /** DB·레거시에서 자주 보이는 깨진 패턴 */ function repairKnownMojibakePatterns(text: string): string { let out = text; const rules: [RegExp, string][] = [ [/CC\s+l(?=\s*[0-9])/gi, 'CCI'], [/1시간/gi, '1시간'], [/시간/gi, '시간'], [/([0-9]+)\s*ë\s*¶/gi, '$1분'], [/ë\s*¶/gi, '분'], [/시/gi, '시'], [/ê°„/gi, '간'], [/é/g, 'é'], [/à /g, ' '], ]; for (const [re, rep] of rules) { out = out.replace(re, rep); } return out; } function hasHangul(text: string): boolean { return /[\uAC00-\uD7A3]/.test(text); } function hasMojibakeMarkers(text: string): boolean { return /[ìíîïðñòóôõö÷øùúûüÿàáâãäåæçèêë]/.test(text) || /ë\s*¶/.test(text) || /시/.test(text) || /ê°„/.test(text) || /Ã.|Â./.test(text); } function looksLikeBrokenKorean(text: string): boolean { if (!text) return false; if (hasHangul(text) && hasMojibakeMarkers(text)) return true; if (hasMojibakeMarkers(text) && !hasHangul(text)) return true; return false; } function normalizeReportText(text: string): string { return text .replace(/,{2,}/g, ', ') .replace(/\s*,\s*,+/g, ', ') .replace(/,\s*,/g, ', ') .replace(/\s{2,}/g, ' ') .trim(); } /** * 깨진 한글·시간봉 표기 복구. 이미 정상 UTF-8 이면 그대로 반환. */ export function repairUtf8Mojibake(text: string): string { if (!text) return text; let result = text; if (hasMojibakeMarkers(result)) { result = repairKnownMojibakePatterns(result); } if (looksLikeBrokenKorean(text)) { const fromBytes = repairFromLatin1Bytes(text); if (fromBytes && hasHangul(fromBytes) && !looksLikeBrokenKorean(fromBytes)) { result = fromBytes; } } if (hasMojibakeMarkers(result)) { result = repairKnownMojibakePatterns(result); } return normalizeReportText(result); }