키 배치

학습 경로가 아니라 참고 문서다. 필요할 때 들어와 찾아보는 곳.

목적

"초성 ㄱ 은 어느 키를 몇 번 누르는가" 같은 심볼 → 키열 매핑을 담는 파일 3종을 설명한다. 규칙표(rules.json)가 무엇을 출력할지를 정한다면, 키맵은 그 글자를 어떤 키로 치는지를 정한다. 생성기는 이 둘을 곱해 manipulator 를 만든다.

파일 내용 항목 수
keymap_390.json(Extensions/n3sh/core/keymap_390.json) 세벌식 390 배열의 심볼 → 키열 79
keymap_final.json(Extensions/n3sh/core/keymap_final.json) 세벌식 최종식 배열의 심볼 → 키열 79
keymap_final_to_390.json(Extensions/n3sh/core/keymap_final_to_390.json) 두 배열의 산출 차이 10건만 기록한 대조표 10

심볼 표기

각 항목은 {"sym": [위치, 문자], "keys": [{key_code, modifiers}, ...]} 형태다.

위치(sym[0]) 의미 문서 표기
cho 초성 접두 없음 —
jung 중성 접두 없음 —
jong 종성 ~ 접두~ㄱ
lit 낱자 그대로 출력하는 항목(ex: 한 글자)
tail 꼬리 — 수식키 조합·기호·spacebar 여부로 결정되는 후행 키열

keys 가 2개 이상이면 연타를 뜻한다(ex: 초성 = k 두 번).

문서 표기 열은 산문에서 그 심볼을 지칭하는 방법이다 — JSON 값이 아니라 사람이 읽는 표기이며, SSOT 는 notation.md "자모" 다. 같은 이라도 초성(k 연타)과 종성(~ㄲ)이 다른 심볼이므로 접두로 가른다.

매핑 정본 — 이 표가 SSOT 다

아래 두 표가 사람 정본이다. 고친 뒤 python3 tools/keymap_build.py buildExtensions/n3sh/core/keymap_{390,final}.json 을 재생성하고 check 로 정합을 확인한다.

셀 표기는 프로젝트 왕복 규약을 따른다 — =빈 문자열, =공백 1칸. 키열key_code 를 공백으로 잇고 shift 가 필요하면 key+shift 로 적는다(빈 키열은 ). tail 행의 다섯 칸 (⌥ ⇧ ⌘ 기호 sp)은 규칙표의 같은 이름 컬럼과 대응하며, (빈 값)과 -(하이픈)는 서로 다른 값이므로 구분해 적는다.

⚠️ 표 위의 <!-- data: ... --> 앵커를 지우면 그 표는 생성 대상에서 빠진다 — 도구가 fail-loud 로 멈춘다.

390 배열

| 종류 | 자모 | ⌥ | ⇧ | ⌘ | 기호 | sp | 키열 | | :--- | :- | : | : | : | :- | :- | :------------------- | | cho | ㄱ | — | — | — | — | — | k | | cho | ㄲ | — | — | — | — | — | k k | | cho | ㄴ | — | — | — | — | — | h | | cho | ㄷ | — | — | — | — | — | u | | cho | ㄸ | — | — | — | — | — | u u | | cho | ㄹ | — | — | — | — | — | y | | cho | ㅁ | — | — | — | — | — | i | | cho | ㅂ | — | — | — | — | — | semicolon | | cho | ㅅ | — | — | — | — | — | n | | cho | ㅇ | — | — | — | — | — | j | | cho | ㅈ | — | — | — | — | — | l | | cho | ㅊ | — | — | — | — | — | o | | cho | ㅋ | — | — | — | — | — | 0 | | cho | ㅌ | — | — | — | — | — | quote | | cho | ㅍ | — | — | — | — | — | p | | cho | ㅎ | — | — | — | — | — | m | | jong | ㄱ | — | — | — | — | — | x | | jong | ㄲ | — | — | — | — | — | 1+shift | | jong | ㄴ | — | — | — | — | — | s | | jong | ㄶ | — | — | — | — | — | s+shift | | jong | ㄷ | — | — | — | — | — | a+shift | | jong | ㄹ | — | — | — | — | — | w | | jong | ㄺ | — | — | — | — | — | d+shift | | jong | ㅀ | — | — | — | — | — | v+shift | | jong | ㅁ | — | — | — | — | — | z | | jong | ㅂ | — | — | — | — | — | 3 | | jong | ㅄ | — | — | — | — | — | x+shift | | jong | ㅅ | — | — | — | — | — | q | | jong | ㅆ | — | — | — | — | — | 2 | | jong | ㅇ | — | — | — | — | — | a | | jong | ㅈ | — | — | — | — | — | 1+shift | | jong | ㅊ | — | — | — | — | — | z+shift | | jong | ㅌ | — | — | — | — | — | w+shift | | jong | ㅍ | — | — | — | — | — | q+shift | | jong | ㅎ | — | — | — | — | — | 1 | | jung | ㅏ | — | — | — | — | — | f | | jung | ㅐ | — | — | — | — | — | r | | jung | ㅑ | — | — | — | — | — | 6 | | jung | ㅓ | — | — | — | — | — | t | | jung | ㅔ | — | — | — | — | — | c | | jung | ㅕ | — | — | — | — | — | e | | jung | ㅗ | — | — | — | — | — | v | | jung | ㅘ | — | — | — | — | — | slash f | | jung | ㅙ | — | — | — | — | — | slash r | | jung | ㅚ | — | — | — | — | — | slash d | | jung | ㅛ | — | — | — | — | — | 4 | | jung | ㅜ | — | — | — | — | — | b | | jung | ㅝ | — | — | — | — | — | 9 t | | jung | ㅟ | — | — | — | — | — | 9 d | | jung | ㅡ | — | — | — | — | — | g | | jung | ㅢ | — | — | — | — | — | 8 | | jung | ㅣ | — | — | — | — | — | d | | lit | , | — | — | — | — | — | comma | | lit | . | — | — | — | — | — | period | | lit | ㄲ | — | — | — | — | — | k k | | lit | ㄸ | — | — | — | — | — | u u | | lit | ㅃ | — | — | — | — | — | semicolon semicolon | | lit | ㅆ | — | — | — | — | — | n n | | lit | ㅉ | — | — | — | — | — | l l | | lit | ㅑ | — | — | — | — | — | 6 | | lit | ㅒ | — | — | — | — | — | r+shift | | lit | ㅖ | — | — | — | — | — | 7 | | lit | ㅘ | — | — | — | — | — | slash f | | lit | ㅙ | — | — | — | — | — | slash r | | lit | ㅚ | — | — | — | — | — | slash d | | lit | ㅝ | — | — | — | — | — | 9 t | | lit | ㅞ | — | — | — | — | — | 9 c | | lit | ㅟ | — | — | — | — | — | 9 d | | lit | ㅠ | — | — | — | — | — | 5 | | lit | ㅢ | — | — | — | — | — | 8 | | tail | — | — | — | — | — | — | — | | tail | — | — | — | — | . | ✔ | period spacebar | | tail | — | - | - | - | — | — | — | | tail | — | - | - | - | — | ✔ | spacebar | | tail | — | - | - | - | . | ✔ | period spacebar | | tail | — | - | - | ✔ | — | — | — | | tail | — | - | - | ✔ | — | ✔ | spacebar | | tail | — | - | - | ✔ | . | ✔ | period spacebar | | tail | — | - | ✔ | - | — | ✔ | spacebar | | tail | — | - | ✔ | - | ? | — | slash+shift spacebar | | tail | — | - | ✔ | - | ? | ✔ | slash+shift spacebar | | tail | — | - | ✔ | ✔ | ! | — | b+shift spacebar | | tail | — | - | ✔ | ✔ | ! | ✔ | b+shift spacebar | | tail | — | ✔ | - | - | — | — | — | | tail | — | ✔ | - | - | — | ✔ | spacebar |

최종식 배열

| 종류 | 자모 | ⌥ | ⇧ | ⌘ | 기호 | sp | 키열 | | :--- | :- | : | : | : | :- | :- | :------------------- | | cho | ㄱ | — | — | — | — | — | k | | cho | ㄲ | — | — | — | — | — | k k | | cho | ㄴ | — | — | — | — | — | h | | cho | ㄷ | — | — | — | — | — | u | | cho | ㄸ | — | — | — | — | — | u u | | cho | ㄹ | — | — | — | — | — | y | | cho | ㅁ | — | — | — | — | — | i | | cho | ㅂ | — | — | — | — | — | semicolon | | cho | ㅅ | — | — | — | — | — | n | | cho | ㅇ | — | — | — | — | — | j | | cho | ㅈ | — | — | — | — | — | l | | cho | ㅊ | — | — | — | — | — | o | | cho | ㅋ | — | — | — | — | — | 0 | | cho | ㅍ | — | — | — | — | — | p | | cho | ㅎ | — | — | — | — | — | m | | jong | ㄱ | — | — | — | — | — | x | | jong | ㄲ | — | — | — | — | — | 1+shift | | jong | ㄴ | — | — | — | — | — | s | | jong | ㄶ | — | — | — | — | — | s+shift | | jong | ㄷ | — | — | — | — | — | a+shift | | jong | ㄹ | — | — | — | — | — | w | | jong | ㄺ | — | — | — | — | — | 2+shift | | jong | ㅀ | — | — | — | — | — | r+shift | | jong | ㅁ | — | — | — | — | — | z | | jong | ㅂ | — | — | — | — | — | 3 | | jong | ㅄ | — | — | — | — | — | x+shift | | jong | ㅅ | — | — | — | — | — | q | | jong | ㅆ | — | — | — | — | — | 2 | | jong | ㅇ | — | — | — | — | — | a | | jong | ㅈ | — | — | — | — | — | 3+shift | | jong | ㅊ | — | — | — | — | — | z+shift | | jong | ㅌ | — | — | — | — | — | w+shift | | jong | ㅍ | — | — | — | — | — | q+shift | | jong | ㅎ | — | — | — | — | — | 1 | | jung | ㅏ | — | — | — | — | — | f | | jung | ㅐ | — | — | — | — | — | r | | jung | ㅑ | — | — | — | — | — | 6 | | jung | ㅓ | — | — | — | — | — | t | | jung | ㅔ | — | — | — | — | — | c | | jung | ㅕ | — | — | — | — | — | e | | jung | ㅗ | — | — | — | — | — | v | | jung | ㅘ | — | — | — | — | — | slash f | | jung | ㅙ | — | — | — | — | — | slash r | | jung | ㅚ | — | — | — | — | — | slash d | | jung | ㅛ | — | — | — | — | — | 4 | | jung | ㅜ | — | — | — | — | — | b | | jung | ㅝ | — | — | — | — | — | 9 t | | jung | ㅟ | — | — | — | — | — | 9 d | | jung | ㅡ | — | — | — | — | — | g | | jung | ㅢ | — | — | — | — | — | 8 | | jung | ㅣ | — | — | — | — | — | d | | lit | . | — | — | — | — | — | period | | lit | ㄲ | — | — | — | — | — | k k | | lit | ㄸ | — | — | — | — | — | u u | | lit | ㅃ | — | — | — | — | — | semicolon semicolon | | lit | ㅆ | — | — | — | — | — | n n | | lit | ㅉ | — | — | — | — | — | l l | | lit | ㅑ | — | — | — | — | — | 6 | | lit | ㅒ | — | — | — | — | — | g+shift | | lit | ㅖ | — | — | — | — | — | 7 | | lit | ㅘ | — | — | — | — | — | slash f | | lit | ㅙ | — | — | — | — | — | slash r | | lit | ㅚ | — | — | — | — | — | slash d | | lit | ㅝ | — | — | — | — | — | 9 t | | lit | ㅞ | — | — | — | — | — | 9 c | | lit | ㅟ | — | — | — | — | — | 9 d | | lit | ㅠ | — | — | — | — | — | 5 | | lit | ㅢ | — | — | — | — | — | 8 | | tail | — | — | — | — | — | — | — | | tail | — | — | — | — | . | ✔ | period spacebar | | tail | — | - | - | - | — | — | — | | tail | — | - | - | - | — | ✔ | spacebar | | tail | — | - | - | - | . | ✔ | period spacebar | | tail | — | - | - | ✔ | — | — | — | | tail | — | - | - | ✔ | — | ✔ | spacebar | | tail | — | - | - | ✔ | . | ✔ | period spacebar | | tail | — | - | ✔ | - | — | ✔ | spacebar | | tail | — | - | ✔ | - | ? | — | slash+shift spacebar | | tail | — | - | ✔ | - | ? | ✔ | slash+shift spacebar | | tail | — | - | ✔ | ✔ | ! | — | b+shift spacebar | | tail | — | - | ✔ | ✔ | ! | ✔ | b+shift spacebar | | tail | — | ✔ | - | - | — | — | — | | tail | — | ✔ | - | - | — | ✔ | spacebar |

왜 역추론에서 선언으로 바꿨나 (2026-08-06)

2026-08-06 이전에는 이 파일이 설명 문서였고, 키맵은 keymap_derive.py(tools/n3sh/keymap_derive.py) 가 원본 오라클에서 역추론했다 — (출력 글자, 실제 키열) 쌍을 제약으로 두고 미지 심볼이 하나만 남는 레코드부터 확정해 나가는 제약 전파다.

그 구조가 막다른 길을 만들었다. 재배치 이후의 오라클이 없는 배열은 자기 코퍼스로 학습할 수 없다 — 390 을 자기 정본으로 학습시키면 5단계 받침의 1↔︎3 한 칸 밀림 때문에 14행이 다른 글자의 오라클 항목에 매칭돼 출력 21행이 바뀌고 5행이 사라진다. 그중 18행은 교집합 밖이라 오라클 대조가 잡지 못하는 조용한 회귀였다.

애초에 세벌식 자모→키 위치는 자판 정의에서 결정적으로 나오는 값이라 추론할 대상이 아니었다. 그래서 표를 정본으로 올리고, 역추론은 검증 수단으로 남긴다.

이전 지금
정본 오라클(역추론 결과) 이 문서의 표
keymap_derive.py 학습기 — 매 실행 재학습 검증기 — 선언값과 대조
배열 분리 불가(오라클 부재) 표를 배열별로 적으면 끝

조립 불가 레코드는 검증에서 제외된다

txt 가 빈 레코드는 cho/jung/jong 심볼 자체가 없어 개별 키로 분해되는 압축식 모델을 따르지 않으므로 역추론 검증에서 뺀다(is_forced). 현재 해당 행은 0건이다.

억지 컬럼은 폐지됐다(2026-08-06). 예전에는 손 축약 타건 16행(390)이 여기 걸려 오라클 literal 로만 산출됐다. 그 행들이 필요로 하던 심볼을 이 문서의 표에 선언하면서 전부 자모 조립으로 넘어갔고, 산출물은 바이트 단위로 동일했다 — 표현이 바뀌었을 뿐 동작은 그대로다.

배열 차이 (keymap_final_to_390.json)

227건 중 217건은 두 배열이 동일하고 10건만 다르다. 차이는 세 패턴으로 나뉜다.

패턴 의미 해당
A 트리거 조합 자체가 다름 (from 이 다름) — 최종 g+f ↔︎ 390 f+r
B ~ㅈ 키가 다름 늦·찾·맞·잊·갖· (최종 3+shift ↔︎ 390 1+shift)
C 종성 겹받침(~ㄺ·~ㅀ) 키가 다름 읽·잃·싫

은 왜 뒤늦게 들어왔나

이 목록은 원래 두 오라클을 직접 대조해 만들었다. 그런데 390 오라클의 에는 390 전환 때 최종식 값이 그대로 남아 있었다(3+shift). 그 결과 두 오라클이 우연히 같은 값이 되어 "차이 없음" 으로 판정됐고, 실제로는 배열 차이 항목인데 목록에서 빠졌다.

오라클에 결함이 있는 항목일수록 오히려 조용히 누락되는 구조였다는 뜻이다. 그래서 지금은 오라클을 대조하지 않고 두 배열 keymap 으로 산출한 결과를 비교해 검증한다.

python3 tools/verify_array_diff.py       # 계산 ↔ 선언 대조 (불일치 시 exit 1)
python3 tools/verify_array_diff.py -v    # 판정 경로(calc/oracle)까지 전건 출력

억지='✔' 레코드(맞·잃·읽)는 압축식 모델 밖이라 오라클 대조로 판정했다(출력의 판정=oracle). ** 이후 그런 레코드는 0건**이라 전건이 계산 경로로 판정된다. 검증기는 diff_count = 실제 항목 수, diff_count + identical_count = total_manipulators 산술도 함께 본다.

패턴 A 는 출력이 아니라 입력 트리거가 달라 오라클 매칭 자체가 실패하므로, 생성기가 이 파일을 배열별 트리거 예외의 SSOT 로 읽는다.

스냅샷 재현성 (2026-07-27 해소)

한동안 keymap_390.json(87 심볼)이 keymap_derive.py 재실행으로 재현되지 않았다(81 심볼). 원인은 스냅샷이 시절 산출물이었기 때문이다 — 그때는 억지 레코드까지 학습에 넣어 실제 자판과 무관한 심볼이 섞였다.

구분 구 스냅샷(87) 현행(81)
lit k+k (억지 축약 타건에서 잘못 학습) 1+shift (실제 키)
cho · jong ··· · jung · lit . 있음 없음 → 2026-08-06 선언으로 부활(아래)
jong 없음 a (정상 학습)

현행 코드로 만든 build 는 오라클과 전건 일치(390 227/최종식 228)하므로 생성기가 옳고 스냅샷이 낡았던 것이다. 2026-07-27 재생성으로 정합화했다.

심볼별 출처 (2026-07-29)

위 표의 "전부 억지 레코드에서만 나오던 것" 이라는 판정은 심볼마다 출처 레코드를 짚어야 재검증할 수 있다. 아래는 각 심볼을 산출하던 레코드 전체다. idxrules.json 레코드 인덱스이며, 억지 열이 인 레코드는 현행 is_forced 가 학습에서 제외한다.

심볼 산출 레코드 (idx · 트리거 · txt) 억지 현행 판정
lit . idx0 period+comma. · idx1 c+v. ✔ ✔ 소거 — 정상 출처 없음
cho idx53 u+1 소거 — 정상 출처 없음
jong idx51 l+period 소거 — 정상 출처 없음
jong idx44 j+- 소거 — 정상 출처 없음
jong idx45 j+comma 소거 — 정상 출처 없음
jong idx38 h+open_bracket · idx49 k+o ✔ ✔ 소거 — 정상 출처 없음
jung idx37 9+s 소거 — 정상 출처 없음
lit idx21 k+j · idx166 1.0+w ✔ · — 유지 — 정상 출처 idx166 이 1+shift 를 준다
jong idx229 u+0 추가 — 처음부터 정상 레코드였고 구 스냅샷이 놓쳤다

읽는 법은 두 가지다.

재현 명령은 다음과 같다.

python3 - <<'PY'
import sys; sys.path.insert(0, 'tools')
from n3sh import keymap_derive as kd
targets = {('cho','ㄸ'), ('jong','ㄱ'), ('jong','ㄺ'), ('jong','ㅄ'),
           ('jong','ㅅ'), ('jung','ㅟ'), ('lit','.'), ('lit','ㄲ'), ('jong','ㅇ')}
for i, r in enumerate(kd.load_records):
    if kd.is_disabled(r):
        continue
    hit = {tuple(s) for s in kd.rec_symbols(r)} & targets
    if hit:
        print(f"idx{i:3d} forced={'Y' if kd.is_forced(r) else 'n'} "
              f"{r.get('c1n')!r}+{r.get('c2n')!r} txt={r.get('txt')!r} {sorted(hit)}")
PY

알려진 원본 결함

관련