fix(B07-CAD): 한글 IME 조합 입력 — 문자 단계 숨은 받개 · 조합 중 단축키 건너뜀

- 문자를 기다리는 단계(TEXT · MTEXT · 표 칸 · 지시선)에서 숨은 input 에 초점 · compositionupdate/end 로 글자 받음
- 조합 중 키(isComposing · 229 · Process)는 단축키 처리에서 건너뜀 · 명령줄도 같음
- 명령 단계는 초점을 풀어 한글 자판이어도 영문 명령 그대로
- 입력 해석 패턴을 input-patterns.ts 로 나눔(700줄 상한)
- 검증 창구 entities 에 label · 시나리오 ui_checks/44-8_CAD_한글입력.js

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
This commit is contained in:
2026-10-05 11:32:37 +09:00
co-authored by Claude Opus 5.5
parent bf40ca52a4
commit ac1124b9f1
7 changed files with 227 additions and 24 deletions
@@ -0,0 +1,66 @@
/* 44-8 CAD 한글 입력 — orca eval 한 번에 도는 시나리오.
* 시작 화면: http://localhost:5183/b07-cad/index.html (CAD 앱 단독 · 빈 도면 · 저장 안 함)
* 또는 CAD iframe 을 품은 화면(B07 · M02 양식).
* 하는 일: 키보드로 TEXT 명령 → 캔버스 한 점 클릭 → 숨은 받개(.cad-ime-sink) 초점 →
* 조합 사건(keydown Process · compositionstart/update/end)으로 「가나다」 → Enter → 글자 객체 → Ctrl+Z 되돌림.
* 기대: sinkFocused true · imeKeyPrevented 0 · label "가나다" · 명령 끝 뒤 sinkReleased true ·
* 되돌린 뒤 textCount = 시작 값.
*/
(async () => {
const W = (t) => new Promise((r) => setTimeout(r, t));
const w = document.querySelector("iframe")?.contentWindow ?? window;
const d = w.document;
const cad = w.__aisloCad;
const texts = () => cad.entities().filter((e) => e.type === "Text");
const before = texts().length;
const key = (el, opts) => {
const e = new w.KeyboardEvent("keydown", { bubbles: true, cancelable: true, ...opts });
el.dispatchEvent(e);
return e.defaultPrevented;
};
for (const k of ["T", "E", "X", "T", "Enter"]) key(d.body, { key: k });
const canvas = d.querySelector("canvas");
const r = canvas.getBoundingClientRect();
const at = { clientX: r.left + r.width / 2, clientY: r.top + r.height / 2, bubbles: true };
canvas.dispatchEvent(new w.MouseEvent("mouseenter", at));
canvas.dispatchEvent(new w.MouseEvent("mousemove", at));
canvas.dispatchEvent(new w.MouseEvent("mousedown", { ...at, button: 0 }));
canvas.dispatchEvent(new w.MouseEvent("mouseup", { ...at, button: 0 }));
canvas.dispatchEvent(new w.MouseEvent("mousemove", { ...at, clientX: at.clientX + 3 }));
await W(300);
const sink = d.querySelector(".cad-ime-sink");
const sinkFocused = !!sink && d.activeElement === sink;
let imeKeyPrevented = 0;
const comp = (type, data) =>
sink.dispatchEvent(new w.CompositionEvent(type, { data, bubbles: true }));
for (const [jamo, syllable] of [
["ㄱ", "가"],
["ㄴ", "나"],
["ㄷ", "다"],
]) {
imeKeyPrevented += key(sink, { key: "Process", keyCode: 229 });
comp("compositionstart", "");
comp("compositionupdate", jamo);
imeKeyPrevented += key(sink, { key: "Process", keyCode: 229, isComposing: true });
comp("compositionupdate", syllable);
comp("compositionend", syllable);
}
key(sink, { key: "Enter" });
await W(100);
const made = texts().slice(before);
const label = made.length ? made[made.length - 1].label : null;
canvas.dispatchEvent(new w.MouseEvent("mousemove", { ...at, clientX: at.clientX + 6 }));
await W(300);
const sinkReleased = d.activeElement !== sink;
key(d.body, { key: "Escape" });
if (made.length) key(d.body, { key: "z", ctrlKey: true });
await W(100);
return JSON.stringify({
sinkFocused,
imeKeyPrevented,
label,
sinkReleased,
textBefore: before,
textAfterUndo: texts().length,
});
})();