Files
Aislo/A00_Common/key_registry.ts
T
eomsangdonandClaude Opus 5.5 2e4316567c feat(설정 단축키): CAD 리본 묶음 · 찾기 · 다른 곳 = 실제 동작 등록부 표 · 키 바꾸기 · 겹침 막음 (PLAN 588-1~3 · 요구 445 C2 · C3)
- 새 key_registry.ts — 화면마다 받는 키 · 마우스 동작 한 벌 · keyIs(evt, id) · 바꾼 키만 input-prefs.keys
- ☰ [설정] CAD 탭 = 찾기 칸 + 「키 · 마우스」 + 리본 탭 › 패널 접이 묶음(기타 여섯) · 날 input → 부품 입력
- 다른 곳 탭 = 손글 안내 지우고 등록부 표(B05 방향키 · D03 Delete 등 빠진 것 포함) · 찾기
- 키 기록 칸 · 줄마다 [기본값] · [모두 기본값] · 같은 화면 · 모든 화면 겹침 · 브라우저 몫 · CAD 글자 단독은 저장 막음
- 배선: B05 되돌림 · 자유지정 Enter · Backspace · D03 되돌림 · 그리기 · 지움(한글 자판도 자판 자리) · CAD F3 · F7 · F8 · F10 · Ctrl 명령 · Delete
- 낡은 글 대조 시험 셋(215-1 · 345-3 · 346-4)은 새 배선 글로 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 18:50:41 +09:00

347 lines
21 KiB
TypeScript

/* =============================================================================
* key_registry.ts
* 단축키 등록부(PLAN 588-2 · 3) — 화면마다 실제로 받는 키 · 마우스 동작 한 벌 · ☰ [설정] 표가 여기서 그려짐.
*
* - 줄 = 동작 id · 자리 · 기본 키 · 이름 · `fixed`(못 바꿈 — Esc · 방향키 · 마우스 · 글 입력 · 시트 키)
* - 판정 = `keyIs(evt, id)` — 사용자가 바꾼 키(`input-prefs` 의 `keys`) 있으면 그것만 · 없으면 기본 키
* - 키 글 = 「Ctrl+Shift+Z」 꼴 — 글자 · 숫자는 자판 자리(`keyLetter` · 한글 자판 안전) · 차례 Ctrl · Alt · Shift · 키
* - 겹침 = 같은 화면에서 함께 듣는 자리끼리 · 「모든 화면」과 · 브라우저 몫 · CAD 글자 단독(명령행 몫)
* - 불러올 때 window 안 만짐(CAD 도 import) · 시험 = `resources/tester/test_588_key_registry.py`
* ========================================================================== */
import { inputPrefs } from "./input_prefs";
import { keyLetter } from "./key_shortcut";
export type KeyScope =
| "all"
| "b05"
| "profile"
| "routeEdit"
| "drainage"
| "section"
| "plan"
| "d02"
| "b04"
| "cad"
| "b08"
| "b09"
| "m02"
| "stab"
| "etc";
type Text = readonly [ko: string, en: string];
export interface KeyAction {
id: string;
scope: KeyScope;
/** 기본 키(키 글) · 못 바꾸는 줄은 보일 글(마우스 · 「글자」 등) */
keys: readonly string[];
label: Text;
fixed?: true;
}
/** 자리 이름 · 함께 듣는 화면(같은 화면이면 키가 겹치면 안 됨) — 표 차례도 이대로 */
export const KEY_SCOPES: Record<KeyScope, { label: Text; page: string }> = {
all: { label: ["모든 화면", "All views"], page: "all" },
b05: { label: ["B05 · B06 공통", "B05 · B06 common"], page: "b05" },
profile: { label: ["B05 종단", "B05 profile"], page: "b05" },
routeEdit: { label: ["B05 노선 편집(평면)", "B05 route edit (plan)"], page: "b05" },
drainage: { label: ["B05 배수 · 3D", "B05 drainage · 3D"], page: "b05" },
section: { label: ["B06 횡단", "B06 cross section"], page: "b05" },
plan: { label: ["D03 노선분석", "D03 route analysis"], page: "plan" },
d02: { label: ["D02 기준지정", "D02 base points"], page: "d02" },
b04: { label: ["B04 지도 · 3D", "B04 map · 3D"], page: "b04" },
cad: { label: ["CAD", "CAD"], page: "cad" },
b08: { label: ["B08 수량 · 이정표", "B08 quantity · milestones"], page: "b08" },
b09: { label: ["B09 내역", "B09 estimate"], page: "b09" },
m02: { label: ["M02 양식", "M02 templates"], page: "m02" },
stab: { label: ["E03 · E04 · E07 안정성", "E03 · E04 · E07 stability"], page: "stab" },
etc: { label: ["그 밖", "Other"], page: "etc" },
};
const ARROWS = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"] as const;
const SHIFT_ARROWS = ARROWS.map((key) => `Shift+${key}`);
/** 화면 옮기기 — ☰ [설정] 마우스 탭에서 고른 버튼(가운데는 늘) */
const PAN = "옮기기 버튼 끌기";
const A = (
id: string,
scope: KeyScope,
keys: readonly string[],
label: Text,
fixed?: true,
): KeyAction => (fixed ? { id, scope, keys, label, fixed } : { id, scope, keys, label });
const F = true as const;
/* prettier-ignore */
export const KEY_ACTIONS: readonly KeyAction[] = [
A("all.escape", "all", ["Escape"], ["☰ · 창 · 메뉴 닫기 · 고름 풀기", "☰ · close window · menu · clear selection"], F),
A("all.loupe", "all", ["Z"], ["누른 채 = 돋보기", "Hold = loupe"], F),
A("all.loupeRight", "all", ["오른쪽 0.4초 누름"], ["돋보기", "Loupe"], F),
A("all.loupeWheel", "all", ["돋보기 중 휠"], ["돋보기 배율", "Loupe zoom"], F),
A("all.zoomPan", "all", [PAN], ["그림 칸 화면 옮김(횡단 · 단면 · 미리 보기)", "Pan drawings (sections · previews)"], F),
A("all.zoomReset", "all", ["두 번 누름"], ["그림 칸 배율 처음대로", "Reset drawing zoom"], F),
A("all.resizer", "all", ["칸 경계 두 번 누름"], ["칸 크기 처음대로", "Reset panel size"], F),
A("all.grip", "all", ["ArrowLeft", "ArrowRight", "Shift+ArrowLeft", "Shift+ArrowRight"], ["칸 경계 손잡이에서 크기 바꿈", "Resize at a panel grip"], F),
A("all.touchLong", "all", ["손가락 길게 누름"], ["오른쪽 누름(메뉴)", "Right click (menu)"], F),
A("all.touchDouble", "all", ["손가락 두 번 톡"], ["두 번 누름", "Double click"], F),
A("all.touchPinch", "all", ["두 손가락 벌림"], ["확대", "Zoom in"], F),
A("b05.undo", "b05", ["Ctrl+Z"], ["되돌림(평면 · 종단 · 횡단 · 관 · 구조물)", "Undo (plan · profile · section · pipes · structures)"]),
A("b05.redo", "b05", ["Ctrl+Shift+Z", "Ctrl+Y"], ["다시", "Redo"]),
A("b05.escape", "b05", ["Escape"], ["모든 보기의 고름 풀기", "Clear selection in every view"], F),
A("profile.grade", "profile", ["ArrowUp", "ArrowDown"], ["계획고 0.1 m · [쉬프트] 중 구간 평행 옮김 · 누른 채 = 되풀이", "Grade 0.1 m · parallel shift · hold = repeat"], F),
A("profile.station", "profile", ["ArrowLeft", "ArrowRight"], ["구조물 · 부정 측점 옮김(종단을 마지막에 누른 때)", "Move structure · irregular station"], F),
A("profile.freeFinish", "profile", ["Enter"], ["자유지정 끝", "Finish free mode"]),
A("profile.freeBack", "profile", ["Backspace"], ["자유지정 마지막 점 빼기", "Free mode: remove last point"]),
A("profile.escape", "profile", ["Escape"], ["자유지정 취소 · 직선화 시작 측점 취소", "Cancel free mode · straighten start"], F),
A("profile.wheel", "profile", ["휠"], ["가로 스크롤(그래프 · 유토곡선 · 측점 표)", "Horizontal scroll"], F),
A("profile.drag", "profile", ["왼쪽 끌기", PAN], ["가로 옮김", "Horizontal pan"], F),
A("profile.menu", "profile", ["오른쪽 누름"], ["구조물 메뉴(넣기 · 지움)", "Structure menu"], F),
A("profile.pill", "profile", ["알약 끌기", "알약 오른쪽 누름"], ["구조물 옮김 · 지움", "Move · delete structure"], F),
A("profile.marker", "profile", ["측점 표식 끌기"], ["측점 옮김", "Move station"], F),
A("profile.markerPick", "profile", ["Enter", "Space"], ["초점 측점 표식 고름", "Pick focused station marker"], F),
A("profile.balloon", "profile", ["풍선 두 번 누름"], ["유토곡선 풍선 자동 자리", "Balloon back to auto"], F),
A("profile.hold", "profile", ["▲▼ 누른 채"], ["되풀이(0.5초 뒤 초당 10번)", "Repeat while held"], F),
A("routeEdit.nudge", "routeEdit", ARROWS, ["교각점 0.1 m(측점 맞춤이면 한 칸) · 고른 마커 옮김", "Move IP 0.1 m · picked marker"], F),
A("routeEdit.nudgeBig", "routeEdit", SHIFT_ARROWS, ["교각점 1 m 옮김", "Move IP 1 m"], F),
A("routeEdit.unmark", "routeEdit", ["Escape"], ["마커 고름 풀기", "Clear marker pick"], F),
A("routeEdit.zoom", "routeEdit", ["휠"], ["커서 자리 확대 · 축소", "Zoom at cursor"], F),
A("routeEdit.rotate", "routeEdit", ["Ctrl+휠"], ["지도 5° 돌림", "Rotate map 5°"], F),
A("routeEdit.pan", "routeEdit", [PAN], ["화면 옮김", "Pan"], F),
A("routeEdit.measure", "routeEdit", ["Shift+누름"], ["재기", "Measure"], F),
A("routeEdit.drag", "routeEdit", ["노드 · 손잡이 끌기"], ["교각점 옮김 · 반지름 바꿈", "Move IP · change radius"], F),
A("routeEdit.tick", "routeEdit", ["측점 눈금 누름"], ["그 측점 횡단 미리 보기", "Preview that station's section"], F),
A("routeEdit.splice", "routeEdit", ["선 두 번 누름"], ["노드 넣기", "Add node"], F),
A("routeEdit.unsplice", "routeEdit", ["노드 오른쪽 누름"], ["노드 지움", "Delete node"], F),
A("routeEdit.menu", "routeEdit", ["노선 오른쪽 누름"], ["구조물 메뉴 · 마커 지움", "Structure menu · delete marker"], F),
A("routeEdit.contour", "routeEdit", ["등고선 누름"], ["등고선 고정 단추", "Contour lock button"], F),
A("drainage.zoom", "drainage", ["휠"], ["배수유역도 확대 · 축소", "Drainage map zoom"], F),
A("drainage.pan", "drainage", [PAN], ["배수유역도 옮김", "Drainage map pan"], F),
A("drainage.pipe", "drainage", ["관 표식 끌기"], ["관 옮김", "Move pipe"], F),
A("drainage.basin", "drainage", ["누름"], ["유역 고름 · 풂", "Pick · unpick basin"], F),
A("drainage.menu", "drainage", ["오른쪽 누름"], ["배수 · 3D 노선 메뉴", "Drainage · 3D route menu"], F),
A("drainage.orbit", "drainage", ["3D 왼쪽 끌기"], ["누른 점 둘레로 돌림", "Orbit around picked point"], F),
A("drainage.pan3d", "drainage", [`3D ${PAN}`], ["3D 화면 옮김", "3D pan"], F),
A("drainage.zoom3d", "drainage", ["3D 휠"], ["3D 커서 자리 확대", "3D zoom at cursor"], F),
A("section.pad", "section", ARROWS, ["열린 조정 판 ▲▼◀▶", "Open adjust pad ▲▼◀▶"], F),
A("section.zoom", "section", ["Ctrl+휠"], ["카드 확대(휠만 = 카드 목록 스크롤)", "Card zoom (wheel alone scrolls)"], F),
A("section.detailEnter", "section", ["Enter"], ["상세 창 고르기 칸에 넣음", "Detail: put the pick in the cell"], F),
A("section.detailEscape", "section", ["Escape"], ["상세 창 고르기 취소", "Detail: cancel picking"], F),
A("section.detailAdd", "section", ["Shift+누름", "Ctrl+누름"], ["상세 창 선 조각 · 면 더함", "Detail: add line pieces · faces"], F),
A("section.detailMenu", "section", ["표 칸 오른쪽 누름"], ["상세 표 칸 메뉴", "Detail table cell menu"], F),
A("section.hold", "section", ["▲▼ 누른 채"], ["선 칸 되풀이", "Line cell repeat"], F),
A("plan.undo", "plan", ["Ctrl+Z"], ["되돌림", "Undo"]),
A("plan.redo", "plan", ["Ctrl+Shift+Z", "Ctrl+Y"], ["다시", "Redo"]),
A("plan.drawFinish", "plan", ["Enter"], ["그리기 끝", "Finish drawing"]),
A("plan.drawBack", "plan", ["Backspace"], ["그리기 마지막 점 빼기", "Drawing: remove last point"]),
A("plan.delete", "plan", ["Delete"], ["고른 것 지움", "Delete selection"]),
A("plan.escape", "plan", ["Escape"], ["그리기 취소 · 고름 풀기 · 재기 끝", "Cancel drawing · clear selection · end measure"], F),
A("plan.zoom", "plan", ["휠"], ["커서 자리 확대 · 축소", "Zoom at cursor"], F),
A("plan.rotate", "plan", ["Ctrl+휠"], ["지도 돌림", "Rotate map"], F),
A("plan.pan", "plan", [PAN], ["화면 옮김", "Pan"], F),
A("plan.vertex", "plan", ["고른 선 누름", "꼭짓점 끌기"], ["꼭짓점 넣고 끌기 · 옮김", "Insert · drag vertex"], F),
A("plan.unvertex", "plan", ["꼭짓점 · 마커 오른쪽 누름"], ["지움", "Delete"], F),
A("plan.measure", "plan", ["재기: 누름 · 누름"], ["시점 · 종점", "Start · end"], F),
A("d02.place", "d02", ["지도 누름"], ["지점 놓기", "Place point"], F),
A("d02.drag", "d02", ["표식 끌기"], ["지점 옮김", "Move point"], F),
A("b04.zoom", "b04", ["휠"], ["지도 · 3D 확대 · 축소", "Map · 3D zoom"], F),
A("b04.pan", "b04", [PAN], ["지도 · 3D 옮김", "Map · 3D pan"], F),
A("b04.pick", "b04", ["누름"], ["유역 · 흐름 점 고름", "Pick basin · flow point"], F),
A("b04.pipe", "b04", ["관 표식 끌기"], ["관 옮김", "Move pipe"], F),
A("b04.menu", "b04", ["오른쪽 누름"], ["노선 · 관 메뉴", "Route · pipe menu"], F),
A("b04.orbit", "b04", ["3D 왼쪽 끌기"], ["누른 점 둘레로 돌림", "Orbit around picked point"], F),
A("cad.snap", "cad", ["F3"], ["스냅", "Snap"]),
A("cad.grid", "cad", ["F7"], ["그리드", "Grid"]),
A("cad.ortho", "cad", ["F8"], ["직교", "Ortho"]),
A("cad.polar45", "cad", ["F10"], ["45°", "45°"]),
A("cad.undo", "cad", ["Ctrl+Z"], ["되돌림", "Undo"]),
A("cad.redo", "cad", ["Ctrl+Y", "Ctrl+Shift+Z"], ["다시", "Redo"]),
A("cad.save", "cad", ["Ctrl+S"], ["저장(M02)", "Save (M02)"]),
A("cad.selectAll", "cad", ["Ctrl+A"], ["모두 고름", "Select all"]),
A("cad.delete", "cad", ["Delete"], ["고른 것 지움", "Delete selection"]),
A("cad.copy", "cad", ["Ctrl+C"], ["복사", "Copy"]),
A("cad.copyBase", "cad", ["Ctrl+Shift+C"], ["기준점 복사", "Copy with base point"]),
A("cad.cut", "cad", ["Ctrl+X"], ["잘라 냄", "Cut"]),
A("cad.pasteBlock", "cad", ["Ctrl+Shift+V"], ["블록으로 붙여넣기", "Paste as block"]),
A("cad.properties", "cad", ["Ctrl+1"], ["특성", "Properties"]),
A("cad.paste", "cad", ["Ctrl+V"], ["붙여넣기", "Paste"], F),
A("cad.enter", "cad", ["Enter", "Space", "오른쪽 누름"], ["입력 끝 · 쉴 때 명령 반복", "Submit · repeat command when idle"], F),
A("cad.escape", "cad", ["Escape"], ["명령 취소 · 쉴 때 고름 풀기", "Cancel command · clear selection when idle"], F),
A("cad.type", "cad", ["글자"], ["명령 · 값 입력", "Type command · value"], F),
A("cad.backspace", "cad", ["Backspace"], ["입력 글 지움", "Erase typed text"], F),
A("cad.tab", "cad", ["Tab"], ["명령 후보 돌림 · 동적 입력 칸 넘김", "Cycle candidates · next dynamic field"], F),
A("cad.recall", "cad", ["ArrowUp", "ArrowDown"], ["기록 · 최근 값 · 후보", "History · recent values · candidates"], F),
A("cad.arrowPan", "cad", ["Shift+ArrowUp", "Shift+ArrowDown", "ArrowLeft", "ArrowRight"], ["화면 옮김", "Pan"], F),
A("cad.zoom", "cad", ["휠"], ["확대 · 축소", "Zoom"], F),
A("cad.pan", "cad", [PAN], ["화면 옮김", "Pan"], F),
A("cad.snapMenu", "cad", ["Shift+오른쪽 누름"], ["점 물음에서 스냅 메뉴", "Snap menu at a point prompt"], F),
A("cad.extents", "cad", ["가운데 두 번 누름"], ["범위 줌", "Zoom extents"], F),
A("cad.box", "cad", ["왼쪽 끌기"], ["고르기 상자(오른쪽으로 = 안 · 왼쪽으로 = 걸침)", "Selection window · crossing"], F),
A("b08.zoom", "b08", ["휠"], ["이정표 그림 확대 · 축소", "Milestone diagram zoom"], F),
A("b08.pan", "b08", [PAN, "빈 곳 왼쪽 끌기"], ["이정표 그림 옮김", "Milestone diagram pan"], F),
A("b08.reset", "b08", ["빈 곳 두 번 누름"], ["배율 처음대로", "Reset zoom"], F),
A("b08.leg", "b08", ["구간 글 두 번 누름", "Enter", "Escape"], ["노선 종류 · km 고침 · 적용 · 취소", "Edit road type · km · apply · cancel"], F),
A("b08.box", "b08", ["상자 · 잇는 점 끌기"], ["옮김 · 이음", "Move · link"], F),
A("b08.rowMenu", "b08", ["이정표 표 줄 오른쪽 누름"], ["이음 메뉴", "Link menu"], F),
A("b08.cell", "b08", ["두 번 누름", "Enter", "F2"], ["표 칸 입력 · Enter = 넣음", "Edit table cell · Enter = commit"], F),
A("b09.indent", "b09", ["Tab", "Shift+Tab"], ["내역 줄 들임 · 내어씀", "Indent · outdent row"], F),
A("b09.level", "b09", ["ArrowLeft", "ArrowRight"], ["내역 줄 단 바꿈", "Change row level"], F),
A("b09.move", "b09", ["ArrowUp", "ArrowDown", "Alt+ArrowUp", "Alt+ArrowDown"], ["내역 줄 위 · 아래로", "Move row up · down"], F),
A("b09.edit", "b09", ["Enter", "F2", "두 번 누름"], ["고른 줄 · 칸 입력", "Edit picked row · cell"], F),
A("b09.escape", "b09", ["Escape"], ["입력 끝 · 고름 풀기 · 빨강 표시 지움 · 뜬 창 닫기", "End edit · unpick · clear highlight · close popups"], F),
A("b09.menu", "b09", ["칸 · 줄 오른쪽 누름"], ["칸 · 줄 메뉴", "Cell · row menu"], F),
A("b09.drag", "b09", ["줄 끌기"], ["세로 = 자리 · 가로 = 단", "Vertical = position · horizontal = level"], F),
A("m02.save", "m02", ["Ctrl+S"], ["카드 저장", "Save card"], F),
A("m02.treeMove", "m02", ARROWS, ["나무: ↑↓ 초점 · →← 펼침 · 접음", "Tree: ↑↓ focus · →← expand · collapse"], F),
A("m02.rename", "m02", ["F2"], ["나무 이름 바꿈", "Tree rename"], F),
A("m02.delete", "m02", ["Delete"], ["나무 · 흐름도 고른 것 지움", "Delete selected (tree · data flow)"], F),
A("m02.copy", "m02", ["Ctrl+C", "Ctrl+V"], ["나무 복사 · 붙여넣기", "Tree copy · paste"], F),
A("m02.multi", "m02", ["Ctrl+누름", "Shift+누름"], ["나무 여럿 고름", "Tree multi-select"], F),
A("m02.menu", "m02", ["나무 빈 곳 오른쪽 누름"], ["맨 위 메뉴", "Top menu"], F),
A("m02.zoom", "m02", ["휠", "Ctrl+휠"], ["흐름도 · 노임 그림 배율", "Data flow · labor diagram zoom"], F),
A("m02.pan", "m02", [PAN, "Space+끌기"], ["흐름도 · 노임 그림 옮김", "Data flow · labor diagram pan"], F),
A("m02.boxMove", "m02", ARROWS, ["흐름도 고른 상자 10 px(Shift 50)", "Move selected box 10 px (Shift 50)"], F),
A("m02.undo", "m02", ["Ctrl+Z"], ["흐름도 되돌림", "Data flow undo"], F),
A("m02.escape", "m02", ["Escape"], ["잇기 · 알약 취소 · 크게 보기 줄임 · 열린 창 닫기", "Cancel link · pill · shrink full view · close window"], F),
A("m02.sheet", "m02", ["시트 안"], ["엑셀 키 그대로", "Excel keys"], F),
A("stab.zoom", "stab", ["휠"], ["지도 · 그림 확대 · 축소", "Map · drawing zoom"], F),
A("stab.pan", "stab", ["왼쪽 끌기", PAN], ["E04 · E07 지도 · E03 그림 옮김", "Pan"], F),
A("stab.fit", "stab", ["두 번 누름"], ["전체 보기", "Fit"], F),
A("etc.chat", "etc", ["Enter", "Shift+Enter"], ["B01 지식 물음 보냄 · 줄 바꿈", "B01 chat send · new line"], F),
A("etc.help", "etc", ["두 번 누름", "Ctrl+Enter", "Escape"], ["도움말 고침(관리자) · 저장 · 취소", "Help edit (admin) · save · cancel"], F),
A("etc.cadTab", "etc", ["CAD 탭 두 번 누름"], ["탭 이름 바꿈", "Rename tab"], F),
A("etc.sheet", "etc", ["항목 고르기 표"], ["방향키 · Tab · Enter · F2 · Delete · 글자 = 입력", "Arrows · Tab · Enter · F2 · Delete · typing"], F),
A("etc.toolbar", "etc", ["지도 도구줄 휠"], ["넘친 도구줄 가로 스크롤", "Scroll overflowing toolbar"], F),
];
const BY_ID = new Map(KEY_ACTIONS.map((action) => [action.id, action]));
/** 브라우저 몫 — 못 씀 */
export const RESERVED_KEYS = [
"F5",
"F11",
"F12",
"Ctrl+W",
"Ctrl+T",
"Ctrl+N",
"Ctrl+R",
"Ctrl+Shift+T",
];
const MODIFIERS = new Set(["Control", "Shift", "Alt", "Meta", "AltGraph", "CapsLock", "Process"]);
const NAMED: Record<string, string> = {
" ": "Space",
Spacebar: "Space",
Esc: "Escape",
Del: "Delete",
};
/** 누른 키 → 키 글 · 수정키만 · 한글 조합 = 빈 글 */
export function chordOf(
evt: Pick<KeyboardEvent, "key" | "code" | "ctrlKey" | "altKey" | "shiftKey"> &
Partial<Pick<KeyboardEvent, "metaKey">>,
): string {
const letter = keyLetter(evt);
const raw = NAMED[evt.key] ?? evt.key;
if (!letter && (!raw || MODIFIERS.has(raw))) return "";
const key = letter ? letter.toUpperCase() : raw.length === 1 ? raw.toUpperCase() : raw;
const parts = [
evt.ctrlKey || evt.metaKey ? "Ctrl" : "",
evt.altKey ? "Alt" : "",
evt.shiftKey ? "Shift" : "",
];
return [...parts.filter(Boolean), key].join("+");
}
/** 지금 쓰는 키 — 바꾼 것(`keys`) 있으면 그것 · 없으면 기본 */
export function actionKeys(
id: string,
keys: Record<string, string[]> = inputPrefs().keys,
): readonly string[] {
const action = BY_ID.get(id);
if (!action) return [];
return (!action.fixed && keys[id]) || action.keys;
}
/** 이 키가 그 동작인가 */
export const keyIs = (
evt: Parameters<typeof chordOf>[0],
id: string,
keys?: Record<string, string[]>,
): boolean => {
const chord = chordOf(evt);
return !!chord && actionKeys(id, keys).includes(chord);
};
/** 그 자리 바꿀 수 있는 동작 중 이 키에 맞는 첫 동작 id */
export function keyAction(evt: Parameters<typeof chordOf>[0], scope: KeyScope): string | null {
const chord = chordOf(evt);
if (!chord) return null;
const keys = inputPrefs().keys;
return (
KEY_ACTIONS.find((a) => a.scope === scope && !a.fixed && actionKeys(a.id, keys).includes(chord))
?.id ?? null
);
}
export type KeyError = "dup" | "reserved" | "letter";
/** 함께 듣는 자리인가 — 같은 화면 · 「모든 화면」 */
const together = (a: KeyScope, b: KeyScope): boolean =>
a === "all" || b === "all" || KEY_SCOPES[a].page === KEY_SCOPES[b].page;
/** 바꾼 키 검사 — `{동작 id: 첫 잘못}` · 비면 통과 · 겹침은 바꾼 줄에 붙임(상대가 못 바꾸는 줄이어도) */
export function checkKeys(keys: Record<string, string[]>): Record<string, KeyError> {
const errors: Record<string, KeyError> = {};
const live = KEY_ACTIONS.map((a) => ({ a, list: actionKeys(a.id, keys) }));
for (const { a, list } of live) {
if (a.fixed || !keys[a.id]) continue;
for (const key of list) {
const error: KeyError | null = RESERVED_KEYS.includes(key)
? "reserved"
: a.scope === "cad" && /^(Shift\+)?.$/.test(key)
? "letter"
: live.some(
({ a: b, list: other }) =>
b !== a && together(a.scope, b.scope) && other.includes(key),
)
? "dup"
: null;
if (error) {
errors[a.id] ??= error;
break;
}
}
}
return errors;
}
const LABEL: Record<string, string> = {
ArrowUp: "↑",
ArrowDown: "↓",
ArrowLeft: "←",
ArrowRight: "→",
Escape: "Esc",
};
/** 키 글 → 보일 글(↑ · Esc) */
export const keyLabel = (chord: string): string =>
chord
.split("+")
.map((part) => LABEL[part] ?? part)
.join("+");