@ihabdevteam/core 0.315.0 → 0.321.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +425 -0
  2. package/dist/components.cjs.js +183 -183
  3. package/dist/components.esm.css +2 -2
  4. package/dist/hooks.cjs.js +3 -3
  5. package/dist/i18n.cjs.js +3 -3
  6. package/dist/ihabdevteam-core.cjs.js +187 -187
  7. package/dist/ihabdevteam-core.d.ts +114 -15
  8. package/dist/packages/core/src/components/BarChart.js +16 -1
  9. package/dist/packages/core/src/components/HearingDeviceInfo.js +2 -2
  10. package/dist/packages/core/src/components/PanelRow.js +2 -2
  11. package/dist/packages/core/src/components/Popup/CouponInputPopup.js +4 -2
  12. package/dist/packages/core/src/components/Popup/DeviceSideTable.js +2 -2
  13. package/dist/packages/core/src/components/Popup/ListPopup.d.ts +12 -2
  14. package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.css +19 -0
  15. package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.d.ts +16 -1
  16. package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +8 -6
  17. package/dist/packages/core/src/components/Training/TrainingCardGroup.d.ts +20 -1
  18. package/dist/packages/core/src/components/Training/TrainingCardGroup.js +2 -2
  19. package/dist/packages/core/src/components/Training/TrainingPagePopup.d.ts +15 -0
  20. package/dist/packages/core/src/components/Training/TrainingPagePopup.js +2 -2
  21. package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.d.ts +16 -1
  22. package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +2 -2
  23. package/dist/packages/core/src/components/Training/TrainingStepCard.js +11 -6
  24. package/dist/packages/core/src/components/Training/TrainingStepCardOrder.js +14 -1
  25. package/dist/packages/core/src/components/Training/TrainingStepConfirm.d.ts +3 -1
  26. package/dist/packages/core/src/components/Training/TrainingStepConfirm.js +42 -30
  27. package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +13 -8
  28. package/dist/packages/core/src/components/Training/audioWaits.js +22 -4
  29. package/dist/packages/core/src/components/Training/stepAnswerable.d.ts +82 -0
  30. package/dist/packages/core/src/components/Training/stepAnswerable.js +4 -0
  31. package/dist/packages/core/src/components/Training/useNoiseFade.js +5 -1
  32. package/dist/packages/core/src/components/Word/WordTableBlock.css +25 -0
  33. package/dist/packages/core/src/components/Word/WordTableBlock.js +2 -2
  34. package/dist/packages/core/src/components/wordTableLayout.d.ts +13 -1
  35. package/dist/packages/core/src/components/wordTableLayout.js +13 -1
  36. package/dist/packages/core/src/hooks/useWordAttributeFilter.d.ts +10 -1
  37. package/dist/packages/core/src/hooks/useWordAttributeFilter.js +11 -2
  38. package/dist/packages/core/src/hooks/useWordTrainingFilter.d.ts +6 -4
  39. package/dist/packages/core/src/hooks/useWordTrainingFilter.js +46 -6
  40. package/dist/packages/core/src/i18n/locales/en.json +9 -1
  41. package/dist/packages/core/src/i18n/locales/ko.json +9 -1
  42. package/dist/packages/core/src/utils/phonemePairSearch.js +10 -3
  43. package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +37 -0
  44. package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +60 -3
  45. package/dist/packages/core/src/utils/wordTrainingPool.js +3 -2
  46. package/dist/packages/core/src/utils/wornDate.d.ts +14 -2
  47. package/dist/packages/core/src/utils/wornDate.js +38 -3
  48. package/dist/packages/core/src/version.d.ts +1 -1
  49. package/dist/packages/core/src/version.js +1 -1
  50. package/dist/utils.cjs.js +7 -3
  51. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,431 @@ All notable changes to this project will be documented in this file.
7
7
  > 적은 글이라, 지금 이름으로 고쳐 놓으면 그 시절에 그 이름으로 받은 사람이
8
8
  > 제 이야기를 못 찾는다.
9
9
 
10
+ ## 0.321.0
11
+
12
+ ### (209) `TrainingCardGroup.beforeSort` — 정렬 드롭다운 **왼쪽** 자리
13
+
14
+ 사장님 지시(노션 QA *"단어표, 대립쌍표의 그룹 순서 변경 기능 필요"*): *"그룹 자체를 드래그
15
+ 하거나 **정렬 왼쪽에 올림 내림 버튼**을 넣어서 동작하도록 해줘"*. 그룹 짓기·이름 바꾸기·
16
+ 지우기는 있는데 **차례를 바꿀 길이 없었다.** 머리줄이 `제목 … sort … actions` 라 소비자가
17
+ 쓸 수 있는 자리는 정렬 **오른쪽**(`actions`) 하나뿐이었다.
18
+
19
+ **코어가 여는 것은 자리 하나다.** 사장님이 말씀하신 두 길(끄는 손잡이 · ↑↓ 단추)이 **둘 다
20
+ 이 자리에 든다** — 무엇을 세울지와 차례를 간수하는 것은 부르는 쪽 몫이다(차례는 DB 에
21
+ 조직 공용으로 둔다).
22
+
23
+ - `actions` 와 **같은 감싸개** 안이라 누름이 **접기 토글로 안 번진다.** 밖에 두면 ↑ 를 누를
24
+ 때마다 그 그룹이 접힌다.
25
+ - **정렬이 없어도 혼자 선다** — `sort || actions` 에만 자리를 열면 정렬 없는 목록에서 조용히
26
+ 안 그려진다.
27
+ - 안 주면 지금과 같다.
28
+
29
+ > **낭독기 이름은 부르는 쪽이 단다** — (54)① 와 같은 규칙이다. `위로` 가 그룹 수만큼 서면
30
+ > 단추 목록에서 못 가린다. `'국어' 그룹 위로` 꼴로 지어 넘긴다.
31
+
32
+ > 못은 **"왼쪽" 과 "안 번짐"** 을 함께 센다 — 있나/없나만 세면 오른쪽에 붙여도 초록이라
33
+ > 문서 차례(`compareDocumentPosition`)로 센다. 넷으로 깨 봤다(오른쪽에 두기 · 감싸개 밖에
34
+ > 두기 · 정렬 있어야만 열기 · 번짐 막느라 토글까지 죽이기), 넷 다 운다.
35
+
36
+ > 소비자 할 일: 세 자리(`WordTableGroup` · `TrainingGroupList` · `TrainingPlanListBody`)에
37
+ > `beforeSort` 로 차례 손잡이를 넘긴다. **코어가 열었다고 화면이 바뀌지는 않는다.**
38
+
39
+ ### (210) `모두 선택 해제`·`모두 다른 …으로` 를 **표 바로 아래**로
40
+
41
+ 사장님 지시(대립쌍표 만들기 화면을 짚으시며): *"모두 선택해제, 모두 다른 대립쌍으로 버튼을
42
+ **표 바로 아래**에 두도록 해줘"*.
43
+
44
+ **가름은 처음부터 있었고, 자리가 그 가름을 안 보였다.** 액션 줄 가운데 주석에 *"표를 손보는
45
+ 단추들 끝 — 아래부터는 표를 드나드는 단추들이다"* 라고 적어 두고도 둘을 **한 줄에** 뒀다.
46
+ 그 줄은 이름·설명 칸 **아래**라, 손보는 단추가 제가 손보는 표에서 멀찍이 떨어져 섰다
47
+ (말귀 실측 1403 폭: 표 바닥 → 그 줄까지 **440px**). 눈은 표를 보는데 손은 화면 끝으로 간다.
48
+
49
+ 표를 손보는 것 `모두 선택 해제` · `actionsLeftExtra` → **표 바로 아래**(새 줄)
50
+ 표를 드나드는 것 `불러오기` · `새 창으로 보기` · 인쇄 · 저장 → 아래 액션 줄 그대로
51
+
52
+ **고르개를 안 뒀다.** 이 조각을 쓰는 소비자는 한 곳뿐이고(대립쌍표 만들기 · 단어표 만들기),
53
+ **둘 다 옮긴다**(사장님 결정). 고르개를 두면 아무도 안 돌리는 손잡이가 남는다.
54
+
55
+ > 계약 못(`wordTableBlockActionsLeftExtra`)이 묻는 것은 안 바뀌었다 — 덧붙은 것이 *손보는
56
+ > 무리에, `모두 선택 해제` 다음에* 드는가. 바뀐 것은 그 무리가 **어느 줄에 서는가**뿐이다.
57
+ > 새 줄은 **표보다 뒤이고 이름칸보다 앞**인지를 둘 다 묻는다 — 하나만 물으면 **반쯤 되돌려도
58
+ > 안 운다**(실제로 그 반쪽 되돌림을 깨서 확인했다).
59
+
60
+ > 소비자 할 일: **없다.** `actionsLeftExtra` 는 그대로 넘기면 새 자리에 선다.
61
+
62
+ ### (211) 그 줄을 **가운데**로, 단추는 **48** 로
63
+
64
+ 사장님 지시(화면 보시고): *"가운데 배치"* · *"size 48"*. (210) 으로 옮긴 줄이 왼쪽 끝에
65
+ 붙어, 가운데 선 종이와 한 덩이로 안 읽혔다(말귀 실측 1403 폭: 줄 폭 1272, 단추 둘이 왼끝).
66
+
67
+ **크기는 이웃을 따라간다.** 아래 액션 줄에 섰을 때 40 이었던 것은 맞았다 — 거기서는
68
+ `불러오기`·`새 창으로 보기` 와 한 무리였다. 표 밑으로 오면서 **이웃이 바뀌었고**, 부르는
69
+ 쪽이 `actionsLeftExtra` 로 넘기는 짝이 48 이라 40 으로 두면 한 줄에서 키가 어긋난다.
70
+ **자리를 옮기면서 크기를 안 따져 본 것이 (210) 의 빠뜨림이다.**
71
+
72
+ 놀이터 실측: `justify-content: center` · 단추 `button48` · **줄 가운데 647 = 단추 가운데
73
+ 647 = 표 가운데 647** — 세 축이 같다. 폰에서 두 칸 격자는 그대로다(`1fr` 두 칸이라 남는
74
+ 자리가 없어 가운데가 아무 일도 안 한다).
75
+
76
+ > 아래 액션 줄은 `space-between` 그대로 둔다 — 거기는 **양끝에 갈라 세우는** 줄이다.
77
+
78
+ > 소비자 할 일: 없다. 짝이 되는 단추를 48 로 넘기면 키가 맞는다(말귀는 이미 올렸다).
79
+
80
+ ## 0.320.0
81
+
82
+ ### (206) 여럿 고르기의 `확인` 단추를 걷었다 — **내는 자리가 하나가 됐다**
83
+
84
+ 사장님 지시: *"확인버튼 제거 하라고 했어"* · *"UX 바꾸지 마"*(뒤 말은 **고치는 방식**을
85
+ 바꾸지 말라는 뜻이다). 내는 자리가 `결과보기` 하나가 되었으니 **갈래가 없어졌다** —
86
+ 이제 다섯 자리가 `canAnswerStep` 하나를 쓴다(`Card` · `ConfirmWithText` · `Confirm` 두
87
+ 갈래 다 · `CardOrder`).
88
+
89
+ **막이 한 줄을 함께 열었다.** 단추만 걷으면 `multiSelect && currentState !== 'active'`
90
+ 가 남아 **고름이 첫 한 번에 얼어붙고**, 그러면 지키라 하신 *"수정할 수 있어야 해"* 를
91
+ **단추를 걷어서 어기는** 꼴이 된다.
92
+
93
+ **빈 집합도 보고한다.** 처음에는 `next.size > 0` 으로 막았는데 제 못에 걸렸다 — 하나를
94
+ 골랐다 되눌러 빼면 화면은 비는데 기록은 `['물']` 로 남았다(옛 결함과 같은 꼴). *"기억이
95
+ 안 난다"* 와 *"안 풀었다"* 는 **값이 아니라 부름 자취**가 가른다 — 한 번도 안 누르면
96
+ `onDone` 이 아예 안 나가고, 골랐다 뺀 사람은 **빈 집합으로** 나간다.
97
+
98
+ **(102) 가 틀렸던 것이 아니다 — 내는 자리가 옮겨졌다.** 그때 세운 두 까닭(① 개수가 과제가
99
+ 되면 안 된다 · ② 덜 기억한 답도 그 사람의 답이다)은 **지금도 안 깨진다.** 못을 그 두
100
+ 까닭으로 다시 적었다 — *단추가 있나* 는 꼴이고, 묻는 것은 **까닭이 아직 서는가**다.
101
+
102
+ > 소비자 할 일: **없다.** 코어가 단추를 안 그리므로 앱은 그대로다.
103
+
104
+ ### (207) `hideNoiseRows` — 소음 두 줄을 **안 그린다**
105
+
106
+ `TrainingPagePopup` → `TrainingPagePopupBottom` → `NoiseSettingsTooltip` 세 자리를 지나는
107
+ 칸 하나. 켜면 `소음 종류`·`소음 비율` 두 줄이 **사라진다**(`전체 소리`·`재생 속도` 는 그대로).
108
+
109
+ - **흐리게가 아니라 안 그린다.** 흐린 줄은 *"지금은 못 바꾼다"* 를 말하는데, 할 말은
110
+ *"이 훈련에는 소음이 없다"* 다.
111
+ - **`noiseOverride` 와 따로다** — 덮개는 *값*, 이 칸은 *보임*. 묶으면 *"덮개는 걸되 줄은
112
+ 보이게"* 를 못 한다(소음 하 문장 듣기의 제안 갈래가 그 꼴이다).
113
+
114
+ > 소비자 할 일: 소음 없는 세 훈련(짧은 이야기 듣기 · 긴 이야기 듣기 · 문장 순서화)에서
115
+ > `hideNoiseRows` 를 켠다. **`noiseOverride` 만으로는 줄이 안 사라진다** — 그쪽은 값이다.
116
+
117
+ ### (208) 소음 두 줄을 감춰도 설정 창 **가로 폭은 그대로**
118
+
119
+ (207) 을 화면에서 보신 사장님이 바로 짚으셨다 — *"팝업 길이가 짧아지네? 짧아지지 않도록"*
120
+ (**가로 폭만**, 높이는 두 줄만큼 줄어든다). 이 창의 폭은 `소음 종류` 세그먼트가 정한다 —
121
+ 보기 다섯 × 단추 최소폭 — 이고 슬라이더 줄은 폭이 없어 그 줄을 따라 늘어난다. 두 줄을
122
+ 빼면 슬라이더가 제 최소폭으로 줄어 **528 → 301** 이 됐다(말귀_2 · 놀이터 실측 같은 값).
123
+
124
+ **숫자를 베끼지 않았다.** `min-width: 528px` 로 박으면 보기 글자(영어)나 단추 최소폭 토큰이
125
+ 바뀔 때 어긋나고 아무도 안 센다. 대신 **같은 세그먼트를 보이지 않게 한 줄 더 세워**
126
+ (`noise-settings__width-ghost`, `aria-hidden` + `inert`, 높이 0) 브라우저가 폭을 재게 했다 —
127
+ 참 줄과 같은 보기 배열·같은 `PanelRow`·같은 크기라 어느 언어에서든 같은 폭이 된다.
128
+
129
+ 놀이터 실측: 두 줄 있을 때 **528 / 530**(패널/툴팁) · 두 줄 없을 때 **528 / 530** · 그림자 높이
130
+ **0**. 폰 폭(375)에서는 `max-width: calc(100vw - 40px)` 가 먼저 걸려 **335**, 넘침 없음
131
+ (`scrollWidth` = 375). 빈 라벨(`""`)로는 라벨 칸(72)이 안 그려져 440 이 나왔다 — 빈 칸(nbsp)으로
132
+ 둔다.
133
+
134
+ > 실측에서 `.panel-row__label` 을 세면 **셋**이 나온다 — 셋째는 그림자의 빈 칸이다. 글자로
135
+ > 세면 둘이다. `[aria-label="소음 종류"]` 는 여전히 0.
136
+
137
+ ### (205) 등급 필터에 `없음` — 등급이 **빈** 단어도 고른다
138
+
139
+ 사장님 지시: *"빈 칸도 인정하는 걸로. 등급은 `없음` 으로 표현해"*. **다섯** 자리를 함께 고쳤다 —
140
+ 코드 목록(`GRADE_NONE`) · 사전(`없음`/`None`) · 필터 줄(`buildGradeSection`, 맨 끝) ·
141
+ 맞추개(`matchesFilters`) · **단어훈련 풀과 훅**(`wordTrainingPool` · `useWordTrainingFilter`).
142
+
143
+ > 처음엔 *넷* 이라 적고 묶었다. 말귀_1 이 재서 다섯째를 찾았다 — 매니저 단어표는 맞는데
144
+ > **단어훈련 페이지**에서는 `없음` 칩이 영영 안 켜지고 `[""]` 가 저장됐다. 단어훈련 풀이 같은
145
+ > 거름 결정을 **두 줄로 따로** 갖고 있었고(`!w.grade || !gradeSet.has(w.grade)`), 훅은 라벨에서
146
+ > 숫자만 남겨 코드를 만들었다. **같은 결정이 세 자리에 적혀 있으면 하나는 안 따라온다** —
147
+ > 거르는 결정을 `matchesGradeCodes` **하나**로 모았고, 못이 *그 밖에 그 결정을 적은 자리가
148
+ > 없는지* 를 글로 센다. 저장돼 있던 `[""]` 는 되읽을 때 버린다(`isGradeCode`).
149
+
150
+ **이것은 값이 아니라 빔이다.** `'1'`~`'4'` 는 단어의 `grade` 에 그 글자가 들어 있지만
151
+ 어느 단어의 `grade` 도 `'none'` 이 아니다 — 가리키는 것은 **`grade` 가 비어 있다**는
152
+ 사실이다(`undefined`·`null`·`''`, 공백만 든 것도). 그래서 맞추개가 이 코드만 `빈등급인가`
153
+ 로 간다.
154
+
155
+ `GRADE_NONE` · `빈등급인가` 는 **공개 API 가 아니다** — `wordAttributeFilterCodes` 모듈
156
+ 전체가 내부다(`wordAttributeValueCodeOf` 도 안 나간다). 소비자가 쓰는 것은 필터 줄을
157
+ 내주는 `buildGradeSection` 쪽이고, 그쪽은 공개다. 처음에 *"새 공개 손잡이 둘"* 이라
158
+ 적었다가 묶은 `.d.ts` 를 세어 보고 고쳤다 — **공개인지는 소스가 아니라 묶은 알맹이가
159
+ 말한다.**
160
+
161
+ > **되거름이 거짓 초록을 만들 수 있던 자리를 막았다.** 맞추개에는 코드로 못 읽은 라벨에서
162
+ > 숫자만 남기는 길이 있는데(`'3등급'` → `'3'`), `'없음'` 은 숫자가 없어 `''` 가 되고 그러면
163
+ > `word.grade === ''` 로 **빈 등급이 우연히 걸린다.** 사전에서 라벨이 빠진 날 *겉보기에는
164
+ > 되는* 꼴이 되어 그 다음 흠을 못 찾는다. **빈 되거름은 아무것도 안 맞춘다.**
165
+
166
+ > 소비자 할 일: 없다. 쓰던 필터 구성은 그대로 읽힌다(`없음` 을 안 고르면 전과 같다).
167
+
168
+ ### `npm run dist:fresh` — 묶인 **때**를 센다
169
+
170
+ 링크로 코어를 보는 쪽이 **"소스는 새것, 몸은 헌것"** 을 읽는 것을 막는다. `src` 에서 가장
171
+ 늦게 고친 파일과 `dist` 들목, 그리고 **걸린 소비자의 산 vite 캐시**까지 견준다.
172
+
173
+ 같은 날 말귀 둘이 같이 속았다 — 판 이름만 `0.319.0` 이고 `dist` 는 48분 헌것이었다.
174
+ **이름으로는 셀 수 없다**(묶으면 `canAnswerStep` → `ra` 로 줄어 **새것이어도 0건**) —
175
+ 그래서 때를 센다. 눈먼 자리도 적어 뒀다: 때는 *"몸이 바뀜"* 과 *"때만 밀림"* 을 못 가른다.
176
+ 놓치면 한 시간, 헛울면 30초 — **헛울기를 골랐다.**
177
+
178
+ `CONSUMING.md §2-0-5` 의 `rm -rf node_modules/.vite-core*` 를 `.vite*` 로 고쳤다 —
179
+ 소비자가 `VITE_CACHE_DIR` 로 딴 이름을 쓰면 **안 잡혔다.** 이름을 짐작하지 않는다.
180
+
181
+ ## 0.319.0
182
+
183
+ ### (201) 을 **재서** 넓혔다 — 넷이 열리고, 하나는 이 집안이 아니었다
184
+
185
+ 말귀가 네 조각을 **화면에서 다 쟀다**(2026-10-12). 짐작으로 넓히지 않고 **잰 것만** 옮겼다:
186
+
187
+ Confirm(한 개 고르기) 오늘의 듣기 › 짧은 이야기 `농구` 뒤 `축구` 를 눌러도 안 옮겨짐 · disabled:false
188
+ CardOrder 단어훈련 › 듣고 순서대로 기억하기 3번을 다시 눌러도 안 풀림
189
+ (앞서 0.318.0) Card · ConfirmWithText
190
+
191
+ **`Confirm` 은 갈래를 갈랐다.** *"여럿 고르기는 `확인` 이 틈을 메운다"* 는 제 면제가 **한 개 고르기
192
+ 갈래에는 안 걸린다**(말귀가 짚었다) — 거기는 `확인` 이 없고 **고르는 순간이 곧 답**이다. 그래서
193
+ 한 개 고르기만 열고, **여럿 고르기는 그대로 막는다**: 고름만 열면 `확인`(`handleMultiSubmit`)이
194
+ 아직 막혀 **선택은 바뀌는데 못 내는** 꼴이 된다 — 옛 막이가 막으려던 바로 그 어긋남이다.
195
+ 그 갈래를 쓰는 화면을 말귀가 **못 찾아 재지 못했으니** 둘을 함께 열어야 한다.
196
+
197
+ **`CardOrder` 는 풀기만 열었다.** 이 갈래는 **마지막 한 장이 단계를 넘기므로**(`every(v => v !== null)`)
198
+ 세 번째를 잘못 누르는 순간 판이 끝났다 — 앞의 둘은 풀리는데 **마지막만 못 푸는** 꼴이다.
199
+ `onDone` 조건은 **그대로 뒀다**(새로 채워 다 찼을 때만) — 그래서 **미완성 답이 점수로 남지 않는다**:
200
+ 풀면 화면만 비고, 고쳐 다시 채우는 순간 `onDone` 이 **고친 차례**로 나간다.
201
+
202
+ ### 제 셈이 틀렸다 — `OrderList` 는 이 집안이 아니다
203
+
204
+ 처음에 같은 막이를 **여섯**으로 셌는데, `TrainingStepOrderList` 의 `!== 'active'` 는 **조작 막이가
205
+ 아니라** `useEffect` 안의 **첫 보고 문**이다((103) 의 `initial`). 글자가 같아서 같은 꼴로 세어졌다 —
206
+ **"글로 자리를 집을 때는 그 글이 하나인지 먼저 센다"** 를 어제 적어 두고 **내 셈에서 밟았다.**
207
+
208
+ 말귀가 재서 바로잡았다: 그 `move` 는 **원래 안 막혀 있고**(끝낸 단계에서 또 옮겨 차례가 바뀌는
209
+ 것을 확인) 한때 `done` 에서 닫았다가 **되돌린 기록**(`92b71ac`, 2026-09-14)이 그 파일에 있다 —
210
+ *한 번 옮기는 순간 단추가 모두 사라져 문장 순서화를 끝까지 맞출 수 없었다.* **셋째 갈래로 적었다**:
211
+ *옮긴 넷 · 아직 막는 하나 · 이 집안 아님 하나.*
212
+
213
+ ### 옛 계약 시험을 **세 번째로** 고쳤다
214
+
215
+ `TrainingStepDoneReadonly` 를 오늘 세 번 고쳤다(한 조각 → 둘 → 넷). 고칠 때마다 **값이 들어와서**
216
+ 고친 것이고, 그 셋을 머리말에 그대로 뒀다 — **"넷이 같아야 한다 로 되돌리지 말 것. 갈라진 것은
217
+ 재서 갈라진 것이다."**
218
+
219
+ ## 0.318.0
220
+
221
+ ### (205) 등급 툴팁을 **한 열쇠에 배열로** 둔다 (말귀 청, 같은 날)
222
+
223
+ 처음엔 `difficultyInfo1`~`5` 로 나눴는데 셋이 나빴다. **①** 말귀 못(`deadI18nKeys`)이 **산 열쇠를
224
+ 죽은 것으로** 셌다 — 그 못은 `a.b.${x}` 처럼 **점 자리에서 잘린 앞머리**를 쓰는 것으로 세는데, 이것은
225
+ *마지막 마디 뒤에 숫자를 붙여* 안 걸린다(그쪽이 얼린 목록에 다섯 줄을 넣어 트리를 돌렸다 — 배열이면
226
+ 그 다섯 줄이 함께 걷힌다). **②** 줄 수가 다섯에 **못 박힌다**. **③** 앱이 이미 배열(`returnObjects`)을 쓴다.
227
+
228
+ `wordPairs.filter.difficultyInfo` 를 **배열 하나**로 두고 `등급설명(t)` 이 받는다 — 사전이 배열을 안
229
+ 주면(옛 사전을 덮어쓴 소비자 등) **한국어 원문으로 물러선다**(빈 줄을 안 띄운다). 재 보고 적는다:
230
+ `returnObjects` 는 배열을 그대로 주고 `defaultValue` 도 배열로 물러선다(i18next 26.4.1).
231
+ 못은 *배열인가 · 숫자 열쇠가 안 남았나 · 두 사전의 줄 수가 같나 · 배열이 아닐 때 물러서나* 를 센다.
232
+
233
+ > **"글로 자리를 집을 때는 그 글이 하나인지 먼저 센다" 의 반대쪽**이다(말귀가 줄인 말). 오늘 두 번은
234
+ > *한 글이 여러 자리*라서 틀렸고, 여기는 **못이 세는 꼴과 부르는 꼴이 어긋나** 산 열쇠가 죽은 것으로
235
+ > 보였다.
236
+
237
+ ### (205) 등급 툴팁 — **세 화면이 한 말을 한다**
238
+
239
+ 코어는 *"국립국어원의 기초 어휘 등급이 결과에 반영됩니다"* **한 줄**이었다. 그 말로는 **1등급이
240
+ 쉬운 쪽인지 어려운 쪽인지도** 알 수 없다. 앱 두 화면에는 이미 다섯 줄이 들어가 있어
241
+ (앱 열쇠 `wordSearch.filter.gradeInfo`) **세 화면이 다른 말**을 하고 있었다 — 사장님이 주신 글을
242
+ 받아 코어 쪽을 **글자까지 같게** 맞췄다(`difficultyInfo1`~`5`).
243
+
244
+ > 말귀가 **앱 열쇠를 새로 세운 것이 맞았다** — 코어 몫을 앱 사전으로 덮으면 *사전은 한 곳이
245
+ > 갖는다* 는 그쪽 못(`i18nOwnership`)이 운다. 그래서 **코어 쪽을 맞추는 것이 유일한 길**이었다.
246
+
247
+ 못은 *다섯 줄이 두 사전에 있나 · 등급 넷과 `등급없음` 을 빠짐없이 말하나 · **부르는 쪽이 다섯을
248
+ 다 쓰나*** 를 센다(하나를 빼먹으면 그 등급만 설명이 없다 — 넷만 쓰게 고쳐 깨뜨려 확인했다).
249
+
250
+ ### (205) 의 `등급없음` 보기는 **아직 안 넣었다**
251
+
252
+ `buildGradeSection` 이 `[1, 2, 3, 4]` 를 박고 있는 것은 맞다. 그런데 고른 보기를 실제로 걸러내는
253
+ 자리(`phonemePairSearch.ts`)가 **라벨에서 숫자만 뽑아** `word.grade` 와 견준다 — `등급없음` 은 그
254
+ 길로는 `''` 가 되고, **등급 없는 낱말의 `grade` 가 자료에 무엇으로 들어 있는지**(빈 글자인지
255
+ `null` 인지)를 모른 채 넣으면 **보이지만 아무것도 못 거르는 보기**가 된다. 건드릴 자리는 넷이다
256
+ (`등급값` · 코드표 · `buildGradeSection` · 맞추는 자리). **그 한 값을 라이브에서 재서 받은 뒤** 넣는다 —
257
+ 짐작으로 넣는 것이 이 저장소에서 가장 오래 가는 흠이다.
258
+
259
+
260
+ ### (201) 한 번 고른 뒤에도 **답을 고칠 수 있다** — QA 가 올린 버그
261
+
262
+ `TrainingStepCard` 가 `currentState !== 'active'` 로 막고 있었다. 그 막이는 **까닭이 있어 들어왔다** —
263
+ 단추는 `!== 'disabled'` 로 열려 있는데 기록은 `=== 'active'` 일 때만 나가서 **화면 선택은 바뀌는데
264
+ 점수는 그대로**였다. 그런데 고르는 순간 `onDone` 이 나가 단계가 넘어가므로 **자기 첫 고름에 자기가
265
+ 막혔다** — 보기 셋에서 손이 미끄러지면 그 판은 그대로 끝났다.
266
+
267
+ **둘 다 틀렸고 바라는 것은 셋째다 — 선택도 바뀌고 점수도 따라간다**(말귀_2 의 가름 그대로).
268
+ 가름을 *"지금 서 있는 단계인가"* 에서 **"아직 닿지 않은 단계인가"** 로 옮겼다
269
+ (`stepAnswerable.canAnswerStep`): `active`·`done` 은 열고 `disabled` 만 막는다. 그리고 **고친 답도
270
+ `onDone` 으로 보낸다** — 그것이 옛 막이의 까닭을 메우는 자리다.
271
+
272
+ ### 고치기 전에 **러너의 셈을 재고** 고쳤다
273
+
274
+ 이미 지난 단계가 다시 알려도 안전한 까닭은 하나뿐이다 — 러너가 차례를
275
+ `stepIndex: Math.max(prev.stepIndex, …)` 로 올린다. 안 재고 고쳤으면 **고친 답이 다음 단계를
276
+ 한 칸 더 밀** 수도 있었다. 그 셈을 못으로 둔다.
277
+
278
+ > **그 못도 한 번 안 울었다.** 처음엔 `Math.max` 를 `toMatch` 하나로 봤는데 **답 길이 둘**이라
279
+ > (쪽 단계 · 자식 단계) 한쪽을 `+1` 로 되돌려도 다른 쪽이 정규식을 채웠다 — **셈으로** 바꿨다
280
+ > (`Math.max` 둘 · 그냥 `+1` 하나는 *다음 단추* 길이라 남는다).
281
+
282
+ ### 엉뚱한 자리를 한 번 고쳤다 — 못이 잡았다
283
+
284
+ `handleSelect` 를 고치려 했는데 **같은 글이 `handlePlayDone` 에도 있어** 첫 자리에 붙었다
285
+ (`onChange` → `onDone` 블록이 글자까지 같다). 그대로였으면 **소리가 끝날 때마다 차례가 나가는**
286
+ 다른 흠이 됐다. 못이 *"고친 답이 안 나갔다"* 로 울어 알았다 — **글로 자리를 집을 때는 그 글이
287
+ 하나인지 먼저 센다.**
288
+
289
+ ### 같은 막이 넷 중 **하나는 재현돼 함께 옮겼다** — `ConfirmWithText`
290
+
291
+ 말귀가 화면에서 쟀다(회원앱 `오늘의 듣기 › 소음 하 문장 듣기`): `안 들렸어요` 뒤에 `잘 들렸어요`
292
+ 를 눌러도 **아무 일도 안 났다.** **여기는 (201) 보다 한 가지가 더 나빴다** — `disabled` 가 `false` 라
293
+ **막혀 보이지도 않는다.** 누를 수 있게 생긴 단추가 반응을 안 하면 쓰는 사람은 *자기가 잘못 눌렀다고
294
+ 여기고 몇 번 더 누른다.* 그리고 여기는 **`확인` 단추가 없어** 둘 중 하나를 누르는 것이 곧 답이라,
295
+ *"여럿 고르기는 확인이 그 틈을 메운다"* 는 면제가 **안 걸린다.** 오늘의 듣기 **첫 칸**이다.
296
+
297
+ ### 남은 셋은 **아직 못 쟀다** — 그대로 둔다
298
+
299
+ `TrainingStepConfirm` · `CardOrder` · `OrderList`. 말귀가 **화면에 못 세워** 못 쟀다(미리보기 칸이
300
+ 1440 을 800 으로 줄여 그려 누를 자리가 어긋났고, 느슨한 선택자가 다른 탭을 눌러 창을 닫았다).
301
+ **겉꼴이 같다고 함께 바꾸지 않는다** — 특히 순서 매기기는 *'고침' 의 뜻*(되돌려 놓는 것이 답인지)
302
+ 부터 봐야 한다.
303
+
304
+ ### **옛 계약 시험이 옛 꼴을 붙들고 있었다** — 갈라진 것을 적었다
305
+
306
+ `TrainingStepDoneReadonly` 가 *"끝낸 단계는 결과만 보여 준다"* 를 **네 조각에 걸친 한 계약**으로
307
+ 못 박아 두었다. 그 글의 **걱정은 그대로 참이다**(화면 선택과 점수가 어긋나면 안 된다) — 다만
308
+ 막아서 푸는 대신 **같이 움직여서** 푼다. 그래서 그 못을 지우지 않고 **갈래가 둘이 된 것**을 적었다:
309
+
310
+ 옮긴 곳 Card · ConfirmWithText 고치면 둘 다 움직인다
311
+ 그대로 Confirm · CardOrder · OrderList 아직 막는다 — 재 보지 않았다
312
+
313
+ 머리말에 **"넷이 같아야 한다 로 되돌리지 말 것"** 과 그 까닭(뜻이 달라서가 아니라 아직 안 재서)을
314
+ 적었다. 셈과 까닭은 `stepAnswerable` 머리말에도 있다.
315
+
316
+ ## 0.317.0
317
+
318
+ ### (301) 미디어 소스를 비우는 **바른 꼴** — 뒷정리가 내던 오류를 걷는다
319
+
320
+ 말귀_1 이 사장님 지시(*"소음 소리 나는지 니가 직접 재봐"*)로 **처음 소리를 재다가** 찾았다.
321
+ 소리는 제대로 났고(문장 play → 18ms 뒤 소음 loop · 문장 ended → 402ms 뒤 소음 pause), 재는 길에
322
+ 이것이 걸렸다 — `el.src = ''` 는 *"소스 없음"* 이 아니라 **"쪽 주소를 소스로 삼아라"** 다:
323
+
324
+ 17888 loadedmetadata src=F03023.mp3 ← 길이 재기는 성공
325
+ 17888 abort src=(없음) ← 뒷정리 직후
326
+ 17894 error code 4 "MEDIA_ELEMENT_ERROR: Empty src attribute"
327
+
328
+ **실패가 아니라 뒷정리가 낸 오류다.** `done` 깃발 덕에 사람에게는 아무 일도 없지만 **문장을
329
+ 불러올 때마다, 소음을 걷을 때마다** 한 번씩 쌓이고, 빈 `src` 는 쪽을 **한 번 더 받으러** 간다.
330
+ `removeAttribute('src')` 뒤 `load()` 로 바꿨다.
331
+
332
+ > **값은 콘솔이 깨끗해지는 것만이 아니다.** 말귀가 **밤새 소리를 못 쟀던 까닭 중 하나가 이 줄**이다 —
333
+ > 미디어 오류가 이미 쌓여 있으니 *"원래 이런가 보다"* 로 지나갔다. **가짜 오류는 진짜 오류를 가린다.**
334
+
335
+ ### 자리는 **둘**이었다
336
+
337
+ 말귀는 `audioWaits.ts` 를 짚었는데, 세어 보니 `useNoiseFade.ts` 의 소음 걷기도 같은 꼴이었다 —
338
+ **하필 그가 재던 그 요소다.** 소음은 훈련마다 서고 걷히니 그 가짜 오류도 그만큼 쌓였다.
339
+
340
+ ### 흉내가 빈약해서 **맞는 코드가 틀린 것처럼** 보였다
341
+
342
+ 고친 뒤 `useNoiseFade` 의 옛 못이 빨개졌다(`expected '/sounds/noise/noise1.mp3' to be ''`).
343
+ `FakeAudio` 에 **`removeAttribute` 가 없어서** 속성을 지워도 `src` 가 그대로 남은 것이다 — 코드가
344
+ 아니라 **재료**가 틀렸다. jsdom 실물로 재 보니 `removeAttribute('src')` 는 `.src` 를 `''` 로 만들고
345
+ `load()` 는 안 터진다. 흉내에 그 둘을 더했다. **흉내가 실물보다 빈약하면 거짓 빨강이다.**
346
+
347
+ 못은 **그 꼴이 다시 안 생기는지**를 센다(`src = ''` 0건 · `removeAttribute` 와 `load` 를 함께 —
348
+ 속성만 지우면 받던 것이 안 끊긴다). 둘 다 깨뜨려 확인했다. 그려진 콘솔은 `LIVE-ONLY-CHECKS` 10번이다.
349
+
350
+ > 말귀_1 이 덧붙인 말: *"저는 「소리는 나겠지, 단추가 열리니까」 로 밤새 넘겼는데 **열리는 것과
351
+ > 나는 것은 다른 일**이었습니다. 재 보니 둘 다 맞긴 했지만, 맞은 것은 **운이었지 확인이 아니었습니다.**"*
352
+
353
+ ### 청 번호가 갈래로 나뉘었다 — 말귀_1 **201~** · 말귀_2 **301~**
354
+
355
+ (117)~(120) 이 두 뜻으로 겹친 뒤(각각 0.315.0 · 0.316.0 으로 나갔다) 말귀 둘이 정했다. 번호는
356
+ `CORE_REQUESTS.md` 에 **절을 쓰면서만** 잡고 다른 문서에서는 가리키기만 한다 — **번호를 잡는 자리와
357
+ 쓰는 자리가 같아야** 서로 묻지 않아도 안 겹친다. 이 글은 (301) 이지만 갈래가 바뀐 첫 판일 뿐
358
+ 새 종류는 아니다. 코어 쪽 대책은 그대로다 — **번호만 적지 않고 무엇인지를 함께 적는다.**
359
+
360
+ ## 0.316.0
361
+
362
+ 말귀_1 의 넷((117)~(120)). 넷 다 **앱에서는 못 고치는** 자리였다(창·칸·단추·꾸밈이 모두 코어 것이다).
363
+ 셋은 낭독기·자판 쪽이고 하나만 눈에 보인다.
364
+
365
+ > **번호가 두 가지를 뜻했다.** 같은 날 말귀_2 도 (117)~(120) 으로 **다른 넷**(영어 복수형 · 주소 오타 ·
366
+ > 바닥글 · 품사 이름표)을 넘겼고 그것은 **0.315.0** 에 나갔다. 번호 공간은 하나(`CORE_REQUESTS.md`)
367
+ > 인데 말귀_2 는 그 번호를 **글로만 집고 절을 안 썼고**, 말귀_1 은 같은 번호로 절을 썼다. 번호만 보면
368
+ > *이미 한 것*과 *안 한 것*이 겹친다 — 그래서 **아래 글과 못은 번호가 아니라 무엇인지로 적는다.**
369
+
370
+ ### (118) 기기 착용일이 **보는 말을 따른다** — 넷 중 유일하게 눈에 보이던 흠
371
+
372
+ `formatWornDate` 가 `2026.09.09` 한 꼴만 냈다. 사장님 지시(2026-10-11 *"날짜 꼴도 하나로 통일해"*)로
373
+ 앱이 다른 화면 날짜를 모은 뒤, **말귀매니저에 남은 점 꼴은 기기 탭 그 둘뿐**이었다 — 한 창 안에서
374
+ 기기 탭의 `2026.09.09` 와 프로필·보고서의 `2026년 9월 9일` 이 나란히 섰다.
375
+
376
+ **소비자 시험의 흉내에 `i18n` 이 필요해진다** — `PanelRow` 가 이제 `i18n.language` 를 읽으므로,
377
+ `useTranslation` 을 `t` 만 주도록 흉내 낸 시험은 **거짓 빨강**이 난다(말귀_1 이 받자마자 넷 겪었다 —
378
+ `i18n: { language: 'ko' }` 한 줄로 풀렸다). *코드가 쓰는 길을 흉내가 안 받아 주면 거짓 빨강이다.*
379
+
380
+ `lang` 을 받아 `formatDateLocale` 로 넘긴다(한국어 `2026년 9월 9일` · 영어 `Sep 9, 2026` — 앱이 모은
381
+ 그 꼴과 같다). **반쪽 값**(연도만 · 연월만)은 그 말로 옮길 수가 없어 점 꼴 그대로다(서버에 월·일이
382
+ `null` 로 오는 옛 기록이 있다). `lang` 을 안 주면 예전 그대로다.
383
+
384
+ **자리는 셋이었다** — 말귀가 둘(`PanelRow` · `DeviceSideTable`)을 짚었는데 `HearingDeviceInfo` 가
385
+ 하나 더 있었다. 셋 다 이미 `useTranslation` 을 쓰고 있어 `i18n.language` 를 그 자리에서 꺼냈다.
386
+
387
+ ### 그리고 **없는 날짜를 조용히 고쳐 보여 주던 것** — 못이 안 울어서 찾았다
388
+
389
+ `new Date('2026-09-31')` 은 **2026-10-01** 로 굴러간다(`2026-02-30` → `2026-03-02`, 실측). 그대로
390
+ 넘겼으면 자료에 적힌 날과 **다른 날**이 화면에 섰다 — **잘못된 값을 보여 주는 것보다 조용히 고쳐
391
+ 보여 주는 것이 나쁘다**(사람이 틀린 줄 모른다). 지은 ISO 가 **그대로 돌아올 때만** 말로 옮긴다.
392
+
393
+ > 이 자리는 **깨뜨려 봤는데 안 울어서** 찾았다. `&& m && d` 조건을 걷어도 조용했고(그때는
394
+ > `2026-00-00` 이 *못 읽는 날짜*라 저절로 떨어졌다), *무엇이 막고 있나* 를 물으니 막는 것이 그
395
+ > 조건이 아니었다 — 그리고 **굴러가는 쪽은 아무도 안 막고 있었다.**
396
+
397
+ ### (117) 쿠폰 글칸에 **틀렸다는 표**를 단다
398
+
399
+ 실측(말귀 2026-10-12): 없는 쿠폰을 넣으면 글귀는 뜨는데 칸은 `aria-invalid`·`aria-describedby`·
400
+ `aria-errormessage` 가 **다 `null`** 이었다. 글귀의 `role="alert"` 덕에 **한 번은 읽히고** 거기서 끝 —
401
+ 낭독기로 그 칸에 다시 서면 아무 표도 없어 *"고치랬는데 뭘 고치지"* 로 멈춘다(친 값은 그대로 남아 있다).
402
+ 눈으로 보는 사람은 칸 아래 빨간 글로 바로 잇지만 소리로 듣는 사람에게는 그 둘이 이어져 있지 않았다.
403
+
404
+ `aria-invalid` 와 `aria-errormessage`(+`aria-describedby`)를 **글귀가 있을 때만** 단다 —
405
+ `aria-errormessage` 는 `aria-invalid` 가 참일 때만 읽히는 자리라 둘을 늘 같이 단다.
406
+
407
+ ### (119) 상세보기 단추가 **카드마다 제 이름**을 갖는다
408
+
409
+ 한 쪽에 차트가 여섯 장 서고 그중 셋이 이 단추를 단다. 이름이 늘 `상세보기` 한 가지라 낭독기로는
410
+ **"Details · Details · Details"** 뿐이었다(앱이 제 손으로 붙이는 고르개는 같은 날 카드 이름을 담도록
411
+ 고쳤는데 **코어가 세우는 셋만** 똑같았다). 바깥에서 더 받지 않는다 — **이미 `titleKey` 를 받고 있다.**
412
+ `trainingStatus.detailFor`(`{{name}} 상세보기` · `{{name}} details`), 제목이 없으면 예전 글자로 떨어진다.
413
+ 머리 단추와 **좁아서 접힌 `⋯` 메뉴**가 같은 값을 쓴다(한쪽만 고치면 좁은 화면에서 갈린다).
414
+
415
+ ### (120) `ListPopup.onSelect` 가 **선택**이 됐다
416
+
417
+ > **쓰는 쪽이 한 걸음 더 해야 끝난다**(말귀_1 실측 2026-10-12에 드러남). 코어가 연 것은 *안 넘길
418
+ > 수 있게* 한 것이고, **넘기면 그 꾸밈은 그대로 선다** — 받고 바로 재니 줄이 여전히
419
+ > `cursor:pointer`·`tabindex="0"`·낭독 설명을 달고 있었다. 앱이 **빈 함수를 계속 넘기고** 있었기
420
+ > 때문이다. 앱 세 자리(`TrainingListPopup` · `customerPickPurposes` · `CustomerPickPopup`)에서
421
+ > 그것을 걷고 나서야 `cursor:auto` · 정지점 없음 · 설명 없음이 됐다. **열어 주는 고침은 "소비자가
422
+ > 무엇을 바꿔야 끝나는가" 를 함께 적어야 한다** — 이 줄이 그 몫이다.
423
+
424
+ 그 값이 그대로 `Table` 의 `onRowClick` 이 되고, `Table` 은 **그것이 있다는 것만 보고** 줄을 꾸민다
425
+ (손가락 커서 · `tabindex="0"` · *"줄을 누르면 자세히 봅니다"*). 그래서 누를 것이 없는 표도 **빈 함수를
426
+ 넘겨야** 했고 그 꾸밈이 그대로 섰다 — 자판으로 쓰는 사람은 **줄마다 멈췄다**(스무 줄이면 스무 번).
427
+ 안 주면 `onRowClick` 도 비어 그 꾸밈이 저절로 사라진다.
428
+
429
+ ### 옛 못 넷이 바뀐 값을 잡았다 — 넷 다 **일부러 바꾼 것**이라 뜻을 새로 적었다
430
+
431
+ `barChartHeadExtra0916`·`chartSectionHeadSegment0916` 은 단추를 **이름 `상세보기` 로** 찾고 있었고
432
+ (카드 이름이 담기니 안 맞는다 — 차례를 지키는 못이라 머리글자로 찾게 고쳤다), `panelRowDevice`·
433
+ `deviceSideTable` 은 **점 꼴 날짜**를 박고 있었다. 넷 다 고친 까닭을 그 자리에 적었다.
434
+
10
435
  ## 0.315.0
11
436
 
12
437
  말귀가 **영어 화면을 눈으로 열세 쪽** 보고 넘긴 넷((117)~(120)). 넘길 때 *"이 종류가 몇 자리인지는