@ihabdevteam/core 0.322.0 → 0.323.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 (196) hide show
  1. package/CHANGELOG.md +517 -0
  2. package/dist/components.cjs.js +183 -183
  3. package/dist/components.esm.css +1 -1
  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 +54 -16
  8. package/dist/packages/core/src/components/Accordion.js +3 -3
  9. package/dist/packages/core/src/components/Alert.js +2 -2
  10. package/dist/packages/core/src/components/BarChart.js +120 -119
  11. package/dist/packages/core/src/components/BottomNav.js +18 -18
  12. package/dist/packages/core/src/components/BulkBar.js +25 -25
  13. package/dist/packages/core/src/components/Button.js +16 -16
  14. package/dist/packages/core/src/components/Calendar.js +17 -17
  15. package/dist/packages/core/src/components/ChartSection.js +30 -30
  16. package/dist/packages/core/src/components/CountTile.js +38 -38
  17. package/dist/packages/core/src/components/DatePicker.js +59 -59
  18. package/dist/packages/core/src/components/Dialog.js +14 -14
  19. package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.js +8 -8
  20. package/dist/packages/core/src/components/FilterPanel.js +6 -6
  21. package/dist/packages/core/src/components/FilterSectionInfo.js +6 -6
  22. package/dist/packages/core/src/components/GameIntro.js +1 -1
  23. package/dist/packages/core/src/components/Icon.js +2 -2
  24. package/dist/packages/core/src/components/ItemActions.js +16 -16
  25. package/dist/packages/core/src/components/LandingHeader.js +6 -6
  26. package/dist/packages/core/src/components/LineChart.js +24 -24
  27. package/dist/packages/core/src/components/Loader.js +5 -5
  28. package/dist/packages/core/src/components/NoticeBox.js +3 -3
  29. package/dist/packages/core/src/components/PageTitle.js +2 -2
  30. package/dist/packages/core/src/components/Pagination.js +4 -4
  31. package/dist/packages/core/src/components/Panel.js +2 -2
  32. package/dist/packages/core/src/components/PanelRow.js +6 -13
  33. package/dist/packages/core/src/components/PassCard/PassCard.js +9 -9
  34. package/dist/packages/core/src/components/PassCard/passCardProduct.js +2 -2
  35. package/dist/packages/core/src/components/PaymentPlan.js +5 -5
  36. package/dist/packages/core/src/components/Popup/ConfirmPopup.js +29 -29
  37. package/dist/packages/core/src/components/Popup/CouponInputPopup.js +2 -2
  38. package/dist/packages/core/src/components/Popup/DeviceSideTable.js +18 -25
  39. package/dist/packages/core/src/components/Popup/ListPopup.js +3 -3
  40. package/dist/packages/core/src/components/Popup/LoadTablePopup.js +7 -7
  41. package/dist/packages/core/src/components/Popup/Popup.js +27 -27
  42. package/dist/packages/core/src/components/Popup/SaveTablePopup.js +5 -5
  43. package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +4 -4
  44. package/dist/packages/core/src/components/Popup/customerDetailUtils.js +18 -18
  45. package/dist/packages/core/src/components/Popup/useDiscardGuard.js +4 -4
  46. package/dist/packages/core/src/components/ProgressIndicator.js +5 -5
  47. package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +16 -16
  48. package/dist/packages/core/src/components/Rating.js +20 -20
  49. package/dist/packages/core/src/components/Report/ReportDocument.js +21 -21
  50. package/dist/packages/core/src/components/RingChart.js +5 -5
  51. package/dist/packages/core/src/components/SectionHeadFloat.js +15 -15
  52. package/dist/packages/core/src/components/SegmentedControl.js +34 -34
  53. package/dist/packages/core/src/components/SideNav.js +37 -37
  54. package/dist/packages/core/src/components/Slider.js +2 -2
  55. package/dist/packages/core/src/components/SoundCompass.js +5 -5
  56. package/dist/packages/core/src/components/Table.js +257 -98
  57. package/dist/packages/core/src/components/Tabs.js +12 -12
  58. package/dist/packages/core/src/components/TextField.js +34 -34
  59. package/dist/packages/core/src/components/Toast.js +6 -6
  60. package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +4 -4
  61. package/dist/packages/core/src/components/Tooltip/Tooltip.js +22 -22
  62. package/dist/packages/core/src/components/Tooltip/TrainingTooltip.js +4 -4
  63. package/dist/packages/core/src/components/TopBanner.js +28 -28
  64. package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +11 -11
  65. package/dist/packages/core/src/components/Training/CategoryIcon.js +2 -2
  66. package/dist/packages/core/src/components/Training/NoisePlayerContext.js +12 -12
  67. package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +2 -2
  68. package/dist/packages/core/src/components/Training/TrainingBlock.js +11 -11
  69. package/dist/packages/core/src/components/Training/TrainingBlockButton.js +10 -10
  70. package/dist/packages/core/src/components/Training/TrainingBlockGrid.js +21 -21
  71. package/dist/packages/core/src/components/Training/TrainingCard.css +13 -1
  72. package/dist/packages/core/src/components/Training/TrainingCard.js +2 -2
  73. package/dist/packages/core/src/components/Training/TrainingCardGroup.js +6 -6
  74. package/dist/packages/core/src/components/Training/TrainingPagePopup.js +81 -81
  75. package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +17 -17
  76. package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +127 -127
  77. package/dist/packages/core/src/components/Training/TrainingStepCard.js +18 -18
  78. package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +4 -4
  79. package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +12 -12
  80. package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +12 -12
  81. package/dist/packages/core/src/components/Training/TrainingStepTitle.js +4 -4
  82. package/dist/packages/core/src/components/Training/TrainingWordCard.js +17 -17
  83. package/dist/packages/core/src/components/Training/WordTablePopup.js +5 -5
  84. package/dist/packages/core/src/components/Training/audioPlaybackRules.js +8 -8
  85. package/dist/packages/core/src/components/Training/audioTimeline.js +25 -25
  86. package/dist/packages/core/src/components/Training/oneShotSound.js +13 -13
  87. package/dist/packages/core/src/components/Training/trainingMissStats.js +20 -20
  88. package/dist/packages/core/src/components/Training/trainingPopupUtils.js +7 -7
  89. package/dist/packages/core/src/components/Training/useAudioPreview.js +15 -15
  90. package/dist/packages/core/src/components/Training/useFloatingFocus.js +14 -14
  91. package/dist/packages/core/src/components/Training/useNoiseFade.js +22 -22
  92. package/dist/packages/core/src/components/Training/useObjectUrls.js +2 -2
  93. package/dist/packages/core/src/components/Training/usePlaybackSequencer.d.ts +2 -2
  94. package/dist/packages/core/src/components/Training/usePlaybackSequencer.js +23 -23
  95. package/dist/packages/core/src/components/Training/useProgressTicker.js +33 -33
  96. package/dist/packages/core/src/components/Training/useTTSPlayback.js +17 -17
  97. package/dist/packages/core/src/components/Training/useWaveReady.js +11 -11
  98. package/dist/packages/core/src/components/UpdateAlert.js +6 -6
  99. package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +7 -1
  100. package/dist/packages/core/src/components/Word/TableFilterPanel.js +3 -3
  101. package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +10 -10
  102. package/dist/packages/core/src/components/Word/WordTableBlock.js +28 -28
  103. package/dist/packages/core/src/components/Word/WordTableButton.css +16 -2
  104. package/dist/packages/core/src/components/Word/WordTableThumbnail.css +6 -1
  105. package/dist/packages/core/src/components/Word/generatedDraft.js +14 -14
  106. package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -7
  107. package/dist/packages/core/src/components/Word/wordScoreStats.js +15 -15
  108. package/dist/packages/core/src/components/Word/wordTablePrint.js +10 -10
  109. package/dist/packages/core/src/components/Word/wordTableRecommend.js +11 -11
  110. package/dist/packages/core/src/components/Word/wordTableReview.js +28 -28
  111. package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +8 -11
  112. package/dist/packages/core/src/components/Word/wordTableSettings.js +48 -36
  113. package/dist/packages/core/src/components/WordTable.js +130 -130
  114. package/dist/packages/core/src/components/barChartData.d.ts +16 -1
  115. package/dist/packages/core/src/components/barChartData.js +49 -29
  116. package/dist/packages/core/src/components/barChartTypes.js +8 -8
  117. package/dist/packages/core/src/components/tableTrackList.js +11 -11
  118. package/dist/packages/core/src/components/updateAlertStack.js +28 -28
  119. package/dist/packages/core/src/components/useBarChartControl.js +4 -6
  120. package/dist/packages/core/src/components/useComboboxSuggestions.js +17 -17
  121. package/dist/packages/core/src/components/useToastLifecycle.js +18 -18
  122. package/dist/packages/core/src/components/wordTableLayout.d.ts +17 -4
  123. package/dist/packages/core/src/components/wordTableLayout.js +5 -5
  124. package/dist/packages/core/src/context/I18nProvider.js +6 -6
  125. package/dist/packages/core/src/hooks/focusReturn.js +27 -27
  126. package/dist/packages/core/src/hooks/inputModality.js +13 -13
  127. package/dist/packages/core/src/hooks/touchToggle.js +39 -39
  128. package/dist/packages/core/src/hooks/useAnchoredMenu.js +30 -30
  129. package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +28 -28
  130. package/dist/packages/core/src/hooks/useDocumentTitle.js +10 -10
  131. package/dist/packages/core/src/hooks/useEditableList.js +4 -4
  132. package/dist/packages/core/src/hooks/useFocusTrap.js +2 -2
  133. package/dist/packages/core/src/hooks/useFontScale.js +6 -6
  134. package/dist/packages/core/src/hooks/useIcon.js +7 -7
  135. package/dist/packages/core/src/hooks/useListControls.js +2 -2
  136. package/dist/packages/core/src/hooks/usePageResetOnChange.js +3 -3
  137. package/dist/packages/core/src/hooks/usePasswordChange.js +4 -4
  138. package/dist/packages/core/src/hooks/usePendingPress.js +7 -7
  139. package/dist/packages/core/src/hooks/useRelativeDay.js +4 -4
  140. package/dist/packages/core/src/hooks/useSoundCompassExam.js +6 -6
  141. package/dist/packages/core/src/hooks/useTablePaging.d.ts +0 -11
  142. package/dist/packages/core/src/hooks/useTablePaging.js +29 -34
  143. package/dist/packages/core/src/hooks/useTrainingSessionLog.js +35 -35
  144. package/dist/packages/core/src/hooks/useVisitSchedule.js +4 -4
  145. package/dist/packages/core/src/hooks/useWordAttributeFilter.js +14 -14
  146. package/dist/packages/core/src/hooks/useWordMeanings.js +5 -5
  147. package/dist/packages/core/src/hooks/useWordTrainingFilter.js +7 -7
  148. package/dist/packages/core/src/i18n/locales/en.json +13 -2
  149. package/dist/packages/core/src/i18n/locales/ko.json +12 -1
  150. package/dist/packages/core/src/types/wordSearch.d.ts +9 -0
  151. package/dist/packages/core/src/utils/audioMeter.js +5 -5
  152. package/dist/packages/core/src/utils/audioPeaks.d.ts +1 -1
  153. package/dist/packages/core/src/utils/audioPeaks.js +18 -18
  154. package/dist/packages/core/src/utils/blockGrid.js +4 -4
  155. package/dist/packages/core/src/utils/chunkSkew.js +9 -9
  156. package/dist/packages/core/src/utils/clipboard.js +3 -3
  157. package/dist/packages/core/src/utils/dailySelectionUtils.js +12 -12
  158. package/dist/packages/core/src/utils/date.js +12 -12
  159. package/dist/packages/core/src/utils/devWarn.js +4 -4
  160. package/dist/packages/core/src/utils/deviceKind.d.ts +37 -7
  161. package/dist/packages/core/src/utils/deviceKind.js +53 -10
  162. package/dist/packages/core/src/utils/errorMessage.js +7 -7
  163. package/dist/packages/core/src/utils/filterPanelSummary.js +14 -14
  164. package/dist/packages/core/src/utils/format.js +4 -4
  165. package/dist/packages/core/src/utils/getTrainingData.js +9 -9
  166. package/dist/packages/core/src/utils/imageConfig.js +17 -17
  167. package/dist/packages/core/src/utils/koreanSyllable.js +18 -18
  168. package/dist/packages/core/src/utils/navScroll.js +2 -2
  169. package/dist/packages/core/src/utils/phonemeConvert.d.ts +7 -0
  170. package/dist/packages/core/src/utils/phonemeConvert.js +19 -10
  171. package/dist/packages/core/src/utils/phonemeGroups.js +2 -2
  172. package/dist/packages/core/src/utils/phonemePairSearch.d.ts +1 -0
  173. package/dist/packages/core/src/utils/phonemePairSearch.js +118 -102
  174. package/dist/packages/core/src/utils/phonemeSectionCodes.d.ts +52 -0
  175. package/dist/packages/core/src/utils/phonemeSectionCodes.js +76 -0
  176. package/dist/packages/core/src/utils/prefetchChunk.js +6 -6
  177. package/dist/packages/core/src/utils/safeHref.js +4 -4
  178. package/dist/packages/core/src/utils/savedWordTables.js +5 -5
  179. package/dist/packages/core/src/utils/speak.js +12 -12
  180. package/dist/packages/core/src/utils/textSort.d.ts +0 -10
  181. package/dist/packages/core/src/utils/textSort.js +38 -48
  182. package/dist/packages/core/src/utils/theme.js +3 -3
  183. package/dist/packages/core/src/utils/trainingItemResults.js +3 -3
  184. package/dist/packages/core/src/utils/ttsConfig.js +2 -2
  185. package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +45 -45
  186. package/dist/packages/core/src/utils/wordContrast.js +3 -3
  187. package/dist/packages/core/src/utils/wordPairIndex.js +4 -4
  188. package/dist/packages/core/src/utils/wordTrainingGenerators.js +4 -4
  189. package/dist/packages/core/src/utils/wordTrainingPool.js +15 -15
  190. package/dist/packages/core/src/utils/wordTrainingRuntime.js +6 -6
  191. package/dist/packages/core/src/utils/wornDate.d.ts +3 -3
  192. package/dist/packages/core/src/utils/wornDate.js +14 -14
  193. package/dist/packages/core/src/version.d.ts +1 -1
  194. package/dist/packages/core/src/version.js +1 -1
  195. package/dist/utils.cjs.js +4 -4
  196. package/package.json +1 -1
@@ -67,10 +67,10 @@ export function formatPhone(raw) {
67
67
  * **사람에게 거는 번호** — 휴대폰 · 지역번호(02·0NN) · 050X 안심번호 · 0303 착신전환.
68
68
  * 새 대역은 여기에만 더하면 아래 둘이 함께 받는다.
69
69
  */
70
- const 개인번호 = String.raw `(?:0\d{1,2}|050\d|0303)-\d{3,4}-\d{4}`;
70
+ const PERSONAL_PHONE = String.raw `(?:0\d{1,2}|050\d|0303)-\d{3,4}-\d{4}`;
71
71
  /** **대표번호**(1588 등) — 거는 곳이 사람이 아니라 기관이다 */
72
- const 대표번호 = String.raw `1[5-9]\d{2}-\d{4}`;
73
- export const PHONE_REGEX = new RegExp(`^(?:${개인번호}|${대표번호})$`);
72
+ const SERVICE_PHONE = String.raw `1[5-9]\d{2}-\d{4}`;
73
+ export const PHONE_REGEX = new RegExp(`^(?:${PERSONAL_PHONE}|${SERVICE_PHONE})$`);
74
74
  /**
75
75
  * **대표번호를 뺀 것만** 받는 자 — `PHONE_REGEX` 에서 대표번호 갈래를 뺀 것이다.
76
76
  *
@@ -81,7 +81,7 @@ export const PHONE_REGEX = new RegExp(`^(?:${개인번호}|${대표번호})$`);
81
81
  * 않는 대역이 하나 더 생기는 날 **두 앱이 조용히 그것을 받게 된다.** 뜻대로 적어 둔다.
82
82
  * @public
83
83
  */
84
- export const PERSONAL_PHONE_REGEX = new RegExp(`^${개인번호}$`);
84
+ export const PERSONAL_PHONE_REGEX = new RegExp(`^${PERSONAL_PHONE}$`);
85
85
  /** 연락처가 `formatPhone` 이 끝까지 채워 낸 모양인가 — 규칙은 `PHONE_REGEX`. @public */
86
86
  export function isValidPhone(phone) {
87
87
  return PHONE_REGEX.test(phone);
@@ -106,7 +106,7 @@ export async function loadTrainingData() {
106
106
  }
107
107
  /* 다 읽은 뒤에 한꺼번에 넣는다 — 도중에 형식 오류로 멈추면 이미 넣은 것이 남은 채 재시도가 또 넣어, 부를 때마다 풀에
108
108
  중복이 쌓였다(하루 선택이 중복 문항을 뽑고 기기마다 세트가 달라짐). */
109
- const 받음 = { noise: [], short: [], long: [], order: [] };
109
+ const received = { noise: [], short: [], long: [], order: [] };
110
110
  try {
111
111
  for (const key of Object.keys(map || {})) {
112
112
  const groups = map[key] || [];
@@ -121,16 +121,16 @@ export async function loadTrainingData() {
121
121
  // Preserve original group structure from allTrainingData.json
122
122
  const groupObj = Object.assign({}, gr, { training_type: t, category_0: cat0, category_1: cat1 });
123
123
  if (t === 'LISTEN_IN_NOISE') {
124
- 받음.noise.push(groupObj);
124
+ received.noise.push(groupObj);
125
125
  }
126
126
  else if (t === 'SHORT_STORY') {
127
- 받음.short.push(groupObj);
127
+ received.short.push(groupObj);
128
128
  }
129
129
  else if (t === 'LONG_STORY') {
130
- 받음.long.push(groupObj);
130
+ received.long.push(groupObj);
131
131
  }
132
132
  else if (t === 'ORDERING_SENTENCE') {
133
- 받음.order.push(groupObj);
133
+ received.order.push(groupObj);
134
134
  }
135
135
  }
136
136
  }
@@ -148,10 +148,10 @@ export async function loadTrainingData() {
148
148
  catch { /* SSR 등 */ }
149
149
  return;
150
150
  }
151
- NOISE_DATAS.push(...받음.noise);
152
- SHORT_STORIES.push(...받음.short);
153
- LONG_STORIES.push(...받음.long);
154
- ORDER_SETS.push(...받음.order);
151
+ NOISE_DATAS.push(...received.noise);
152
+ SHORT_STORIES.push(...received.short);
153
+ LONG_STORIES.push(...received.long);
154
+ ORDER_SETS.push(...received.order);
155
155
  // 다 채운 뒤 _loaded 를 먼저 세운다 — 'updated' 를 받은 쪽이 isTrainingDataLoaded() 로 확인하면 아직 false 였다
156
156
  _loaded = true;
157
157
  try {
@@ -13,11 +13,11 @@ const LS_KEY = 'word_image_existence_map_v1';
13
13
  let memMap = null;
14
14
  /* 캐시가 바뀔 때마다 오르는 판 번호 — 통계·'있음' 낱말 집합을 판마다 한 번만 센다(단어훈련 칩 한 번에 유형 수만큼
15
15
  2.3만 칸을 훑었다). 캐시를 바꾸는 곳(확인 결과 담기·일괄 주입·비우기)은 모두 판을 올린다. */
16
- let 캐시판 = 0;
17
- let 통계판 = -1;
18
- let 통계값 = null;
19
- let 있음판 = -1;
20
- let 있음집합 = new Set();
16
+ let cacheVersion = 0;
17
+ let statsVersion = -1;
18
+ let statsValue = null;
19
+ let okVersion = -1;
20
+ let okSet = new Set();
21
21
  const inflight = new Map(); // 중복 fetch 방지
22
22
  function ensureLoaded() {
23
23
  if (memMap)
@@ -80,7 +80,7 @@ export async function checkImageExists(word, homonum) {
80
80
  if (ok === null)
81
81
  return false;
82
82
  map[k] = ok;
83
- 캐시판++;
83
+ cacheVersion++;
84
84
  scheduleSave();
85
85
  return ok;
86
86
  });
@@ -143,24 +143,24 @@ export async function prewarmImageCache(words, options = {}) {
143
143
  */
144
144
  export function getImageExistenceStats() {
145
145
  const map = ensureLoaded();
146
- if (통계판 === 캐시판)
147
- return 통계값 ? { ...통계값 } : null;
146
+ if (statsVersion === cacheVersion)
147
+ return statsValue ? { ...statsValue } : null;
148
148
  const values = Object.values(map);
149
- 통계값 = values.length === 0 ? null : { total: values.length, exists: values.filter(Boolean).length };
150
- 통계판 = 캐시판;
151
- return 통계값 ? { ...통계값 } : null;
149
+ statsValue = values.length === 0 ? null : { total: values.length, exists: values.filter(Boolean).length };
150
+ statsVersion = cacheVersion;
151
+ return statsValue ? { ...statsValue } : null;
152
152
  }
153
153
  /** @internal 동음이의 번호 없이(=1) '이미지 있음' 으로 확인된 낱말 집합 — 캐시 판마다 한 번 만든다. 돌려받은 집합을 고치지 말 것. */
154
154
  export function getImageOkWordSet() {
155
155
  const map = ensureLoaded();
156
- if (있음판 === 캐시판)
157
- return 있음집합;
156
+ if (okVersion === cacheVersion)
157
+ return okSet;
158
158
  const s = new Set();
159
159
  for (const k in map)
160
160
  if (map[k] === true && k.endsWith('_1'))
161
161
  s.add(k.slice(0, -2));
162
- 있음집합 = s;
163
- 있음판 = 캐시판;
162
+ okSet = s;
163
+ okVersion = cacheVersion;
164
164
  return s;
165
165
  }
166
166
  /**
@@ -171,7 +171,7 @@ export function getImageOkWordSet() {
171
171
  export function bulkSetImageExistence(entries) {
172
172
  const map = ensureLoaded();
173
173
  Object.assign(map, entries);
174
- 캐시판++;
174
+ cacheVersion++;
175
175
  try {
176
176
  if (typeof localStorage !== 'undefined') {
177
177
  localStorage.setItem(LS_KEY, JSON.stringify(map));
@@ -182,7 +182,7 @@ export function bulkSetImageExistence(entries) {
182
182
  /** @public 캐시 초기화 (이미지 데이터 변경 시 호출) */
183
183
  export function clearImageExistenceCache() {
184
184
  memMap = {};
185
- 캐시판++;
185
+ cacheVersion++;
186
186
  try {
187
187
  if (typeof localStorage !== 'undefined')
188
188
  localStorage.removeItem(LS_KEY);
@@ -241,10 +241,10 @@ export function wordContainsJungseong(word, jamo) {
241
241
  }
242
242
  /** 음절의 종성이 고른 자모 하나와 맞는지 — 두 모드의 단일 출처. */
243
243
  function jongMatchesJamo(jong, jamo, mode) {
244
- const 글자 = JONGSEONG[jong];
245
- if (글자 === jamo)
244
+ const glyph = JONGSEONG[jong];
245
+ if (glyph === jamo)
246
246
  return true;
247
- return mode === 'phoneme' && finalToPhoneme(글자) === finalToPhoneme(jamo);
247
+ return mode === 'phoneme' && finalToPhoneme(glyph) === finalToPhoneme(jamo);
248
248
  }
249
249
  /** @public 단어가 특정 자모를 종성(coda)으로 포함하는지.
250
250
  * `jongMatch` 로 **글자 그대로**(기본) · **받침 소리**(`'phoneme'`)를 고른다 — `JongMatch` 설명을 본다. */
@@ -373,41 +373,41 @@ export function wordHasConsonantAt(word, parts, position,
373
373
  jongMatch = 'glyph') {
374
374
  if (!word)
375
375
  return false;
376
- const 음절 = [...word].map(parseSyllable).filter((s) => !!s);
377
- if (음절.length === 0)
376
+ const syllables = [...word].map(parseSyllable).filter((s) => !!s);
377
+ if (syllables.length === 0)
378
378
  return false;
379
- const 끝 = 음절.length - 1;
379
+ const last = syllables.length - 1;
380
380
  if (position === 'head') {
381
381
  /* 어두는 **첫 음절의 초성**뿐이다. `ㅇ` 은 음가가 없어 자리를 안 가진다 —
382
382
  `아기` 의 어두초성은 **없다**(`cho: ['ㅇ']` 로 물으면 거짓). */
383
383
  if (!parts.cho)
384
384
  return false;
385
- const c = CHOSEONG[음절[0].cho];
385
+ const c = CHOSEONG[syllables[0].cho];
386
386
  return c !== 'ㅇ' && parts.cho.includes(c);
387
387
  }
388
388
  if (position === 'tail') {
389
389
  /* 어말은 **마지막 받침**뿐이다. 받침이 없으면 자리가 없다. */
390
390
  if (parts.jongNone)
391
- return 음절[끝].jong === 0;
391
+ return syllables[last].jong === 0;
392
392
  if (!parts.jong)
393
393
  return false;
394
- return syllableJongMatches(음절[끝], parts.jong, jongMatch);
394
+ return syllableJongMatches(syllables[last], parts.jong, jongMatch);
395
395
  }
396
396
  /* 어중 — 첫 초성과 마지막 종성을 **뺀** 나머지 자음 전부. 2음절 `선물` 이면
397
397
  ㄴ(0번 종성) · ㅁ(1번 초성) 둘이 어중이다. */
398
- const 초성맞나 = !parts.cho || 음절.some((s, i) => {
398
+ const onsetMatches = !parts.cho || syllables.some((s, i) => {
399
399
  if (i === 0)
400
400
  return false; // 첫 초성은 어두다
401
401
  const c = CHOSEONG[s.cho];
402
402
  return c !== 'ㅇ' && parts.cho.includes(c); // 음가 없는 ㅇ 은 자리를 안 가진다
403
403
  });
404
- const 종성맞나 = parts.jongNone
405
- ? 음절.some((s, i) => i !== 끝 && s.jong === 0)
406
- : !parts.jong || 음절.some((s, i) => i !== 끝 && syllableJongMatches(s, parts.jong, jongMatch));
404
+ const codaMatches = parts.jongNone
405
+ ? syllables.some((s, i) => i !== last && s.jong === 0)
406
+ : !parts.jong || syllables.some((s, i) => i !== last && syllableJongMatches(s, parts.jong, jongMatch));
407
407
  /* 둘 다 줬으면 **둘 다** 있어야 한다(AND) — 하나만 줬으면 그쪽만 참이면 된다 */
408
408
  if (!parts.cho && !parts.jong && !parts.jongNone)
409
409
  return false;
410
- return 초성맞나 && 종성맞나;
410
+ return onsetMatches && codaMatches;
411
411
  }
412
412
  /**
413
413
  * @public 문자열이 음절 패턴 DSL 문법인지 — 쉼표(,) 포함 여부로 판별한다.
@@ -749,12 +749,12 @@ export function searchAndRankWords(items, query) {
749
749
  const headLen = [...q].length;
750
750
  const startsWith = (s) => !!s && m(s.normalize('NFC').slice(0, headLen));
751
751
  // 순위는 두 갈래로 나눠 잇는다(안정 정렬과 같은 순서)
752
- const 앞 = [];
753
- const 뒤 = [];
752
+ const before = [];
753
+ const after = [];
754
754
  for (const w of items) {
755
755
  if (!(m(w.word) || m(w.pronunciation ?? '')))
756
756
  continue;
757
- (startsWith(w.word) || startsWith(w.pronunciation ?? '') ? 앞 : 뒤).push(w);
757
+ (startsWith(w.word) || startsWith(w.pronunciation ?? '') ? before : after).push(w);
758
758
  }
759
- return 앞.concat(뒤);
759
+ return before.concat(after);
760
760
  }
@@ -17,8 +17,8 @@ export function scrollNavTargetToTop(scrollContainer, from = 'Nav') {
17
17
  el.scrollTop = 0;
18
18
  return;
19
19
  }
20
- const 문서 = document.scrollingElement ?? document.documentElement;
21
- if (문서 && 문서.scrollHeight <= 문서.clientHeight) {
20
+ const doc = document.scrollingElement ?? document.documentElement;
21
+ if (doc && doc.scrollHeight <= doc.clientHeight) {
22
22
  devWarnOnce('nav-scroll-container', `[${from}] 본문을 맨 위로 되돌릴 통을 못 찾았다(기본 ${DEFAULT_SCROLL_SELECTOR}) — 창도 구를 수 없어 아무 일도 하지 않는다. scrollContainer 로 통을 알려 준다.`);
23
23
  }
24
24
  window.scrollTo({ top: 0 });
@@ -15,4 +15,11 @@ export declare const VOWEL_TO_PHONEME: Record<string, {
15
15
  export declare function vowelGlidesDiffer(a: string, b: string): boolean;
16
16
  /** 종성 글자 → 음소(대표음) 변환. 매핑에 없으면 원본 유지. */
17
17
  export declare function finalToPhoneme(jong: string): string;
18
+ /**
19
+ * 고른 글자를 음소 꼴로 — **열쇠는 코드로 낸다**(`onset`·`vowel`·`coda`·`glide`).
20
+ *
21
+ * **들어오는 것은 둘 다 받는다** — 저장된 단어표의 고른값은 옛 한글 열쇠(`{ 초성: 'ㄴ' }`)일
22
+ * 수 있다(2026-10-13 코드로 옮기며, 저장된 표를 밀지 않으려고 읽는 쪽을 너그럽게 뒀다).
23
+ * 값(자모·`'ㄴ'`)은 한국어 글자 그 자체라 그대로 둔다 — 바꿀 것은 **열쇠와 빔 값**이다.
24
+ */
18
25
  export declare function convertToPhoneme(sel: Record<string, string>): Record<string, string>;
@@ -1,3 +1,4 @@
1
+ import { toPhonemeCodes, PHONEME_NONE } from './phonemeSectionCodes';
1
2
  export const VALID_GLIDES = {
2
3
  ㅏ: ['j', 'w'], ㅓ: ['j', 'w'], ㅗ: ['j'], ㅜ: ['j'],
3
4
  'ㅔ/ㅐ': ['j', 'w'], ㅣ: ['w', 'ɰ'], ㅡ: [],
@@ -44,21 +45,29 @@ export function vowelGlidesDiffer(a, b) {
44
45
  export function finalToPhoneme(jong) {
45
46
  return JONGSEONG_TO_PHONEME[jong] ?? jong;
46
47
  }
48
+ /**
49
+ * 고른 글자를 음소 꼴로 — **열쇠는 코드로 낸다**(`onset`·`vowel`·`coda`·`glide`).
50
+ *
51
+ * **들어오는 것은 둘 다 받는다** — 저장된 단어표의 고른값은 옛 한글 열쇠(`{ 초성: 'ㄴ' }`)일
52
+ * 수 있다(2026-10-13 코드로 옮기며, 저장된 표를 밀지 않으려고 읽는 쪽을 너그럽게 뒀다).
53
+ * 값(자모·`'ㄴ'`)은 한국어 글자 그 자체라 그대로 둔다 — 바꿀 것은 **열쇠와 빔 값**이다.
54
+ */
47
55
  export function convertToPhoneme(sel) {
56
+ const coded = toPhonemeCodes(sel);
48
57
  const result = {};
49
- if (sel['초성'])
50
- result['초성'] = sel['초성'];
51
- if (sel['모음']) {
52
- const m = VOWEL_TO_PHONEME[sel['모음']];
53
- // 활음을 항상 명시한다('없음' 포함). 단어 분해(decomposePhonemicSyllable)는 활음 없는 모음에도
54
- // 활음:'없음'을 넣으므로, 선택 변환도 대칭으로 맞춰야 활음 대비(예: ㅐ↔ㅙ = 없음↔w)가 매칭된다.
58
+ if (coded.onset)
59
+ result.onset = coded.onset;
60
+ if (coded.vowel) {
61
+ const m = VOWEL_TO_PHONEME[coded.vowel];
62
+ // 활음을 항상 명시한다(빔 포함). 단어 분해(decomposePhonemicSyllable)는 활음 없는 모음에도
63
+ // glide: 빔을 넣으므로, 선택 변환도 대칭으로 맞춰야 활음 대비(예: ㅐ↔ㅙ = 빔↔w)가 매칭된다.
55
64
  // (안 넣으면 ㅐ/ㅔ↔ㅚ/ㅙ 대립이 모음(nucleus)만 같고 대비 섹션이 없어 0건이 됨.)
56
65
  if (m) {
57
- result['모음'] = m.nucleus;
58
- result['활음'] = m.glide ?? '없음';
66
+ result.vowel = m.nucleus;
67
+ result.glide = m.glide ?? PHONEME_NONE;
59
68
  }
60
69
  }
61
- if (sel['종성'])
62
- result['종성'] = JONGSEONG_TO_PHONEME[sel['종성']] ?? sel['종성'];
70
+ if (coded.coda)
71
+ result.coda = JONGSEONG_TO_PHONEME[coded.coda] ?? coded.coda;
63
72
  return result;
64
73
  }
@@ -50,8 +50,8 @@ export function phoneticLabel(t, f) {
50
50
  return f.glide ? `${v} · ${GLIDE_LABEL[f.glide](t)}` : v;
51
51
  }
52
52
  /** 기본값만 돌려주는 t — 옛 `tooltip`(한국어 고정)을 같은 표에서 만들어 두 글이 갈라지지 않게 한다. */
53
- const 한국어 = (_key, { defaultValue }) => defaultValue;
54
- const item = (jamos, phonetic) => ({ jamos, phonetic, tooltip: phoneticLabel(한국어, phonetic) });
53
+ const koTranslate = (_key, { defaultValue }) => defaultValue;
54
+ const item = (jamos, phonetic) => ({ jamos, phonetic, tooltip: phoneticLabel(koTranslate, phonetic) });
55
55
  /** 자음 행: 같은 조음 위치를 공유하는 단일 자모 버튼들. */
56
56
  const consonantRow = (place, jamos) => jamos.map(j => item([j], { place }));
57
57
  /** 모음/병합 아이템: `chars`가 여러 글자면 동음 병합 버튼(예: 'ㅔㅐ' → ['ㅔ','ㅐ']). */
@@ -1,5 +1,6 @@
1
1
  export type { WordEntry, PairFilterKeys, SearchPairsDebugReport } from './phonemePairSearchTypes';
2
2
  import type { WordSearchResultItem } from '../types/wordSearch';
3
+ export { toPhonemeCodes, PHONEME_NONE, type PhonemeSectionCode } from './phonemeSectionCodes';
3
4
  import type { WordEntry } from './phonemePairSearchTypes';
4
5
  import type { PairFilterKeys } from './phonemePairSearchTypes';
5
6
  /** applyFilters/matchesFilters가 공통으로 판정하는 필드 — WordEntry와 WordSearchResultWord