fix: 작업실 채팅에 KaTeX 도입 — 앱 중 유일하게 수식 렌더러가 빠져 있었음

09-24 실측: "12MP $\times$ 60fps $\approx$ 수 Gbps"가 그대로 노출. 다른 앱들
(의사·법률·투자 등)은 전부 vendor katex + katex-inline.js를 실어 다음 앱들은
정상인데 작업실만 없었다. doctor-app과 동일 구성(스타일시트+3스크립트+
katex-inline.js)으로 도입하고 addMsg/스트리밍 완료 시 renderKatexIn 호출.

renderMd의 유니코드 폴백(\times→× 등, 가격 "$19..$29" 보호)은 KaTeX가 없는
환경에서만 발동 — KaTeX 있으면 $...$를 그대로 남겨 renderKatexIn이 맡는다.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
kim
2026-09-24 08:42:32 +09:00
co-authored by Claude Code
parent 12f717c626
commit 01c9de8374
+25 -1
View File
@@ -195,6 +195,11 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
.rb-file-card:hover .rb-file-stl-sel,.rb-file-stl-sel:checked{opacity:1;}
.rb-file-card.stl-selected{outline:2px solid #f59e0b;outline-offset:-1px;}
</style>
<link rel="stylesheet" href="/vendor/katex/katex.min.css">
<script src="/vendor/katex/katex.min.js"></script>
<script src="/vendor/katex/auto-render.min.js"></script>
<script src="/vendor/katex/mhchem.min.js"></script>
<script src="../js/app/katex-inline.js"></script>
</head>
<body>
@@ -1314,6 +1319,7 @@ function addMsg(role,html){
const msgs=document.getElementById('chat-msgs');
const div=document.createElement('div');div.className='rb-msg '+role;
const b=document.createElement('div');b.className='rb-bubble';b.innerHTML=html;
if(typeof renderKatexIn==='function')renderKatexIn(b);
div.appendChild(b);
msgs.appendChild(div);msgs.scrollTop=msgs.scrollHeight;
return b;
@@ -1332,6 +1338,24 @@ function renderMd(text){
s=s.replace(/`([^`]+)`/g,'<code>$1</code>')
.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')
.replace(/^#{1,3}\s+(.+)$/gm,'<h3>$1</h3>');
// 인라인 수식 $...$ — KaTeX가 실려 있으면 삽입 직후 renderKatexIn이 진짜 수식으로
// 그려주므로 그냥 둔다(09-24: 다른 앱들은 전부 KaTeX 실려 있었는데 작업실만 빠져 있었다).
// KaTeX가 없는 환경의 폴백으로만 이 유니코드 치환을 돌린다. 자주 나오는 LaTeX 기호를
// 유니코드로 바꾸고 $를 벗긴다. 내용에 백슬래시(LaTeX 명령)가 없으면 건드리지 않는다 —
// 가격 "$19 ... $29" 짝을 수식으로 먹어버리는 사고 방지([[reference_katex_math_rendering]]).
if(!(typeof window!=='undefined' && typeof renderKatexIn==='function' && window.katex)){
s=s.replace(/\$([^$\n]*\\[^$\n]*)\$/g,(m,body)=>{
let t=body;
t=t.replace(/\\(?:text|mathrm|mathbf)\s*\{([^{}]*)\}/g,'$1');
t=t.replace(/\\times/g,'×').replace(/\\approx/g,'≈').replace(/\\sim/g,'~')
.replace(/\\pm/g,'±').replace(/\\cdot/g,'·').replace(/\\le(?:q)?\b/g,'≤')
.replace(/\\ge(?:q)?\b/g,'≥').replace(/\\neq?\b/g,'≠')
.replace(/\\to\b|\\rightarrow\b/g,'→').replace(/\\leftarrow\b/g,'←')
.replace(/\\deg(ree)?\b/g,'°').replace(/\\mu\b/g,'μ').replace(/\\%/g,'%');
t=t.replace(/\\[a-zA-Z]+/g,'').replace(/\\([{}])/g,'$1').trim();
return t;
});
}
// 리스트 — 연속하는 [-*] 줄을 하나의 <ul>로 묶는다(항목별 <ul>이 따로 생기던 버그 수정).
s=s.replace(/(^|\n)([-*]\s+.+(?:\n[-*]\s+.+)*)/g,(m,lead,grp)=>{
const items=grp.split('\n').map(line=>'<li>'+line.replace(/^[-*]\s+/,'')+'</li>').join('');
@@ -1417,7 +1441,7 @@ async function callChat(message){
let ev;try{ev=JSON.parse(part.slice(6));}catch{continue;}
if(ev.type==='token'||ev.type==='text_delta'){
textBuf+=(ev.text||'');
if(bubble){bubble.innerHTML=renderMd(textBuf);msgs.scrollTop=msgs.scrollHeight;}
if(bubble){bubble.innerHTML=renderMd(textBuf);if(typeof renderKatexIn==='function')renderKatexIn(bubble);msgs.scrollTop=msgs.scrollHeight;}
}
}
}