@ihabdevteam/core 0.321.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 (217) hide show
  1. package/CHANGELOG.md +617 -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 +108 -66
  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/HearingDeviceInfo.d.ts +5 -5
  24. package/dist/packages/core/src/components/HearingDeviceInfo.js +2 -2
  25. package/dist/packages/core/src/components/Icon.js +2 -2
  26. package/dist/packages/core/src/components/ItemActions.js +16 -16
  27. package/dist/packages/core/src/components/LandingHeader.js +6 -6
  28. package/dist/packages/core/src/components/LineChart.js +24 -24
  29. package/dist/packages/core/src/components/Loader.js +5 -5
  30. package/dist/packages/core/src/components/NoticeBox.js +3 -3
  31. package/dist/packages/core/src/components/PageTitle.js +2 -2
  32. package/dist/packages/core/src/components/Pagination.js +4 -4
  33. package/dist/packages/core/src/components/Panel.js +2 -2
  34. package/dist/packages/core/src/components/PanelRow.d.ts +2 -4
  35. package/dist/packages/core/src/components/PanelRow.js +8 -15
  36. package/dist/packages/core/src/components/PassCard/PassCard.js +9 -9
  37. package/dist/packages/core/src/components/PassCard/passCardProduct.js +2 -2
  38. package/dist/packages/core/src/components/PaymentPlan.js +5 -5
  39. package/dist/packages/core/src/components/Popup/ConfirmPopup.js +29 -29
  40. package/dist/packages/core/src/components/Popup/CouponInputPopup.js +2 -2
  41. package/dist/packages/core/src/components/Popup/DeviceListPopup.d.ts +4 -8
  42. package/dist/packages/core/src/components/Popup/DeviceListPopup.js +1 -1
  43. package/dist/packages/core/src/components/Popup/DeviceSideTable.js +19 -26
  44. package/dist/packages/core/src/components/Popup/ListPopup.js +6 -6
  45. package/dist/packages/core/src/components/Popup/LoadTablePopup.js +10 -10
  46. package/dist/packages/core/src/components/Popup/Popup.js +27 -27
  47. package/dist/packages/core/src/components/Popup/SaveTablePopup.js +5 -5
  48. package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +4 -4
  49. package/dist/packages/core/src/components/Popup/customerDetailUtils.js +18 -18
  50. package/dist/packages/core/src/components/Popup/useDiscardGuard.js +4 -4
  51. package/dist/packages/core/src/components/ProgressIndicator.js +5 -5
  52. package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +16 -16
  53. package/dist/packages/core/src/components/Rating.js +20 -20
  54. package/dist/packages/core/src/components/Report/ReportDocument.js +21 -21
  55. package/dist/packages/core/src/components/RingChart.js +5 -5
  56. package/dist/packages/core/src/components/SectionHeadFloat.js +15 -15
  57. package/dist/packages/core/src/components/SegmentedControl.js +34 -34
  58. package/dist/packages/core/src/components/SideNav.js +37 -37
  59. package/dist/packages/core/src/components/Slider.js +2 -2
  60. package/dist/packages/core/src/components/SoundCompass.js +5 -5
  61. package/dist/packages/core/src/components/Table.js +257 -98
  62. package/dist/packages/core/src/components/Tabs.js +12 -12
  63. package/dist/packages/core/src/components/TextField.js +34 -34
  64. package/dist/packages/core/src/components/Toast.js +6 -6
  65. package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +4 -4
  66. package/dist/packages/core/src/components/Tooltip/Tooltip.js +22 -22
  67. package/dist/packages/core/src/components/Tooltip/TrainingTooltip.js +4 -4
  68. package/dist/packages/core/src/components/TopBanner.js +28 -28
  69. package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +11 -11
  70. package/dist/packages/core/src/components/Training/CategoryIcon.js +2 -2
  71. package/dist/packages/core/src/components/Training/NoisePlayerContext.d.ts +7 -4
  72. package/dist/packages/core/src/components/Training/NoisePlayerContext.js +12 -12
  73. package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +2 -2
  74. package/dist/packages/core/src/components/Training/TrainingBlock.js +12 -12
  75. package/dist/packages/core/src/components/Training/TrainingBlockButton.js +10 -10
  76. package/dist/packages/core/src/components/Training/TrainingBlockGrid.js +21 -21
  77. package/dist/packages/core/src/components/Training/TrainingCard.css +13 -1
  78. package/dist/packages/core/src/components/Training/TrainingCard.js +2 -2
  79. package/dist/packages/core/src/components/Training/TrainingCardGroup.js +6 -6
  80. package/dist/packages/core/src/components/Training/TrainingPagePopup.js +83 -83
  81. package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +17 -17
  82. package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +128 -128
  83. package/dist/packages/core/src/components/Training/TrainingStepCard.js +18 -18
  84. package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +4 -4
  85. package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +12 -12
  86. package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +13 -13
  87. package/dist/packages/core/src/components/Training/TrainingStepTitle.js +4 -4
  88. package/dist/packages/core/src/components/Training/TrainingWordCard.js +17 -17
  89. package/dist/packages/core/src/components/Training/WordTablePopup.js +5 -5
  90. package/dist/packages/core/src/components/Training/audioPlaybackRules.js +8 -8
  91. package/dist/packages/core/src/components/Training/audioTimeline.js +25 -25
  92. package/dist/packages/core/src/components/Training/oneShotSound.d.ts +1 -1
  93. package/dist/packages/core/src/components/Training/oneShotSound.js +14 -14
  94. package/dist/packages/core/src/components/Training/stepAnswerable.d.ts +2 -2
  95. package/dist/packages/core/src/components/Training/trainingMissStats.js +22 -22
  96. package/dist/packages/core/src/components/Training/trainingPopupUtils.js +7 -7
  97. package/dist/packages/core/src/components/Training/useAudioPreview.js +15 -15
  98. package/dist/packages/core/src/components/Training/useFloatingFocus.js +14 -14
  99. package/dist/packages/core/src/components/Training/useNoiseFade.js +22 -22
  100. package/dist/packages/core/src/components/Training/useObjectUrls.js +2 -2
  101. package/dist/packages/core/src/components/Training/usePlaybackSequencer.d.ts +2 -2
  102. package/dist/packages/core/src/components/Training/usePlaybackSequencer.js +23 -23
  103. package/dist/packages/core/src/components/Training/useProgressTicker.js +33 -33
  104. package/dist/packages/core/src/components/Training/useTTSPlayback.js +17 -17
  105. package/dist/packages/core/src/components/Training/useWaveReady.js +11 -11
  106. package/dist/packages/core/src/components/UpdateAlert.js +6 -6
  107. package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +7 -1
  108. package/dist/packages/core/src/components/Word/TableFilterPanel.js +3 -3
  109. package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +18 -18
  110. package/dist/packages/core/src/components/Word/WordTableBlock.js +28 -28
  111. package/dist/packages/core/src/components/Word/WordTableButton.css +16 -2
  112. package/dist/packages/core/src/components/Word/WordTableThumbnail.css +6 -1
  113. package/dist/packages/core/src/components/Word/generatedDraft.js +14 -14
  114. package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -7
  115. package/dist/packages/core/src/components/Word/wordScoreStats.js +17 -17
  116. package/dist/packages/core/src/components/Word/wordTablePrint.js +10 -10
  117. package/dist/packages/core/src/components/Word/wordTableRecommend.js +13 -13
  118. package/dist/packages/core/src/components/Word/wordTableReview.js +28 -28
  119. package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +8 -11
  120. package/dist/packages/core/src/components/Word/wordTableSettings.js +48 -36
  121. package/dist/packages/core/src/components/WordTable.js +130 -130
  122. package/dist/packages/core/src/components/barChartData.d.ts +16 -1
  123. package/dist/packages/core/src/components/barChartData.js +49 -29
  124. package/dist/packages/core/src/components/barChartTypes.js +8 -8
  125. package/dist/packages/core/src/components/tableTrackList.js +11 -11
  126. package/dist/packages/core/src/components/updateAlertStack.js +28 -28
  127. package/dist/packages/core/src/components/useBarChartControl.js +4 -6
  128. package/dist/packages/core/src/components/useComboboxSuggestions.js +17 -17
  129. package/dist/packages/core/src/components/useToastLifecycle.js +18 -18
  130. package/dist/packages/core/src/components/wordTableLayout.d.ts +22 -7
  131. package/dist/packages/core/src/components/wordTableLayout.js +5 -5
  132. package/dist/packages/core/src/context/I18nProvider.js +6 -6
  133. package/dist/packages/core/src/hooks/focusReturn.js +27 -27
  134. package/dist/packages/core/src/hooks/inputModality.d.ts +1 -1
  135. package/dist/packages/core/src/hooks/inputModality.js +14 -14
  136. package/dist/packages/core/src/hooks/touchToggle.js +39 -39
  137. package/dist/packages/core/src/hooks/useAnchoredMenu.js +30 -30
  138. package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +28 -28
  139. package/dist/packages/core/src/hooks/useDeviceManagement.d.ts +5 -17
  140. package/dist/packages/core/src/hooks/useDeviceManagement.js +20 -8
  141. package/dist/packages/core/src/hooks/useDocumentTitle.js +10 -10
  142. package/dist/packages/core/src/hooks/useEditableList.js +4 -4
  143. package/dist/packages/core/src/hooks/useFocusTrap.js +2 -2
  144. package/dist/packages/core/src/hooks/useFontScale.js +6 -6
  145. package/dist/packages/core/src/hooks/useIcon.js +7 -7
  146. package/dist/packages/core/src/hooks/useListControls.js +2 -2
  147. package/dist/packages/core/src/hooks/usePageResetOnChange.d.ts +3 -3
  148. package/dist/packages/core/src/hooks/usePageResetOnChange.js +6 -6
  149. package/dist/packages/core/src/hooks/usePasswordChange.js +4 -4
  150. package/dist/packages/core/src/hooks/usePendingPress.js +7 -7
  151. package/dist/packages/core/src/hooks/useRelativeDay.js +4 -4
  152. package/dist/packages/core/src/hooks/useSoundCompassExam.js +6 -6
  153. package/dist/packages/core/src/hooks/useTablePaging.d.ts +7 -7
  154. package/dist/packages/core/src/hooks/useTablePaging.js +40 -34
  155. package/dist/packages/core/src/hooks/useTrainingSessionLog.js +35 -35
  156. package/dist/packages/core/src/hooks/useVisitSchedule.js +4 -4
  157. package/dist/packages/core/src/hooks/useWordAttributeFilter.js +14 -14
  158. package/dist/packages/core/src/hooks/useWordMeanings.js +5 -5
  159. package/dist/packages/core/src/hooks/useWordTrainingFilter.d.ts +3 -1
  160. package/dist/packages/core/src/hooks/useWordTrainingFilter.js +14 -12
  161. package/dist/packages/core/src/i18n/locales/en.json +13 -2
  162. package/dist/packages/core/src/i18n/locales/ko.json +12 -1
  163. package/dist/packages/core/src/types/wordSearch.d.ts +9 -0
  164. package/dist/packages/core/src/utils/audioMeter.js +5 -5
  165. package/dist/packages/core/src/utils/audioPeaks.d.ts +1 -1
  166. package/dist/packages/core/src/utils/audioPeaks.js +18 -18
  167. package/dist/packages/core/src/utils/blockGrid.js +4 -4
  168. package/dist/packages/core/src/utils/chunkSkew.js +9 -9
  169. package/dist/packages/core/src/utils/clipboard.js +3 -3
  170. package/dist/packages/core/src/utils/dailySelectionUtils.js +12 -12
  171. package/dist/packages/core/src/utils/date.js +12 -12
  172. package/dist/packages/core/src/utils/devWarn.d.ts +2 -2
  173. package/dist/packages/core/src/utils/devWarn.js +7 -7
  174. package/dist/packages/core/src/utils/deviceKind.d.ts +37 -7
  175. package/dist/packages/core/src/utils/deviceKind.js +53 -10
  176. package/dist/packages/core/src/utils/errorMessage.js +7 -7
  177. package/dist/packages/core/src/utils/filterPanelSummary.d.ts +1 -1
  178. package/dist/packages/core/src/utils/filterPanelSummary.js +16 -16
  179. package/dist/packages/core/src/utils/format.js +4 -4
  180. package/dist/packages/core/src/utils/getTrainingData.js +9 -9
  181. package/dist/packages/core/src/utils/imageConfig.js +17 -17
  182. package/dist/packages/core/src/utils/koreanSyllable.js +18 -18
  183. package/dist/packages/core/src/utils/navScroll.d.ts +2 -2
  184. package/dist/packages/core/src/utils/navScroll.js +6 -6
  185. package/dist/packages/core/src/utils/phonemeConvert.d.ts +7 -0
  186. package/dist/packages/core/src/utils/phonemeConvert.js +19 -10
  187. package/dist/packages/core/src/utils/phonemeGroups.js +2 -2
  188. package/dist/packages/core/src/utils/phonemePairSearch.d.ts +1 -0
  189. package/dist/packages/core/src/utils/phonemePairSearch.js +118 -102
  190. package/dist/packages/core/src/utils/phonemeSectionCodes.d.ts +52 -0
  191. package/dist/packages/core/src/utils/phonemeSectionCodes.js +76 -0
  192. package/dist/packages/core/src/utils/prefetchChunk.js +6 -6
  193. package/dist/packages/core/src/utils/pressable.d.ts +1 -1
  194. package/dist/packages/core/src/utils/pressable.js +3 -3
  195. package/dist/packages/core/src/utils/safeHref.js +4 -4
  196. package/dist/packages/core/src/utils/savedWordTables.js +5 -5
  197. package/dist/packages/core/src/utils/sessionTab.d.ts +7 -7
  198. package/dist/packages/core/src/utils/sessionTab.js +9 -9
  199. package/dist/packages/core/src/utils/speak.js +12 -12
  200. package/dist/packages/core/src/utils/textSort.d.ts +5 -5
  201. package/dist/packages/core/src/utils/textSort.js +47 -47
  202. package/dist/packages/core/src/utils/theme.js +3 -3
  203. package/dist/packages/core/src/utils/trainingItemResults.js +3 -3
  204. package/dist/packages/core/src/utils/ttsConfig.js +2 -2
  205. package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +2 -2
  206. package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +48 -48
  207. package/dist/packages/core/src/utils/wordContrast.js +3 -3
  208. package/dist/packages/core/src/utils/wordPairIndex.js +4 -4
  209. package/dist/packages/core/src/utils/wordTrainingGenerators.js +4 -4
  210. package/dist/packages/core/src/utils/wordTrainingPool.js +15 -15
  211. package/dist/packages/core/src/utils/wordTrainingRuntime.js +6 -6
  212. package/dist/packages/core/src/utils/wornDate.d.ts +3 -3
  213. package/dist/packages/core/src/utils/wornDate.js +14 -14
  214. package/dist/packages/core/src/version.d.ts +1 -1
  215. package/dist/packages/core/src/version.js +1 -1
  216. package/dist/utils.cjs.js +4 -4
  217. package/package.json +1 -1
@@ -1,13 +1,43 @@
1
1
  /**
2
- * 기기 종류(보청기·인공와우·기타)에 붙는 배지 색.
2
+ * 기기 종류(보청기·인공와우·기타) — **이름과 색을 한 자리에서 정한다.**
3
3
  *
4
- * 표(DeviceSideTable)와 '사용중인 기기' 패널(PanelRowDevice)이 같은 값을 각자
5
- * 그리므로 **규칙을 한 곳에 둔다** — 두 곳에서 따로 정하면 같은 인공와우가 한쪽은
6
- * 파랑, 한쪽은 초록으로 떠서 다른 것인 줄 알게 된다.
4
+ * 표(`DeviceSideTable`)와 '사용중인 기기' 패널(`PanelRowDevice`)이 같은 값을 각자 그리므로
5
+ * 규칙을 한 곳에 둔다 — 두 곳에서 따로 정하면 같은 인공와우가 한쪽은 파랑, 한쪽은 초록으로
6
+ * 떠서 다른 것인 줄 알게 된다.
7
7
  *
8
- * 값은 저장된 그대로의 한국어다(managed_device.type). 목록에 없는 값·빈 값은
9
- * 'default'(무채색)로 떨어뜨린다 — 모르는 종류를 아는 색으로 칠하지 않는다.
8
+ * **색만 모여 있고 이름은 안 모여 있었다** — `deviceTypeKeys`·`deviceTypeLabel` 이 그 두
9
+ * 파일에 **주석까지 똑같이 두 벌** 있었다(2026-10-13 말귀가 짚었다). 색에 적어 둔 그 까닭이
10
+ * 이름에도 그대로 들어맞는 자리다. 모았다.
10
11
  *
11
- * @public
12
+ * ## 값은 **코드**다 — 옛 한글 값도 받는다
13
+ *
14
+ * 저장값(`managed_device.type`)이 `'보청기'` 에서 `'hearingAid'` 로 옮겨 가는 중이다
15
+ * (사장님 지시 "저장된 한글 값도 다 영어로"). 앱이 판을 돌리는 때와 이 판이 깔리는 때가
16
+ * 어긋나고, 옛 자료를 보는 다른 기관이 있을 수 있어 **둘 다 받는다**.
17
+ *
18
+ * 받지 않으면 **터지지 않고 조용히 틀린다** — 배지에 `hearingAid` 가 날것으로 뜨고
19
+ * (`defaultValue: device` 로 떨어진다), 색이 둘 다 무채색이 되어 종류를 색으로 가르던 것이
20
+ * 사라진다. 눈으로만 잡히는 꼴이다.
21
+ */
22
+ export type DeviceKind = 'hearingAid' | 'cochlearImplant' | 'other';
23
+ /**
24
+ * 저장값을 종류 코드로 — 모르는 값은 `null`.
25
+ *
26
+ * **모르는 것을 `'other'` 로 접지 않는다.** 기타는 "기타" 라고 고른 값이고, 모르는 값은
27
+ * 아직 뜻을 모르는 것이다. 둘을 섞으면 새 종류가 생긴 날 조용히 '기타' 로 보인다.
28
+ */
29
+ export declare function deviceKindOf(device: string | null | undefined): DeviceKind | null;
30
+ /**
31
+ * 배지 색. 목록에 없는 값·빈 값은 `'default'`(무채색)로 떨어뜨린다 —
32
+ * **모르는 종류를 아는 색으로 칠하지 않는다.**
12
33
  */
13
34
  export declare function deviceKindVariant(device: string | null | undefined): string;
35
+ /**
36
+ * 배지에 적을 이름 — **지금 언어로**. 사전(`device.type.*`)이 쥔다.
37
+ *
38
+ * 모르는 값은 **저장된 그대로** 보인다. 숨기지 않는다 — 화면에 날것이 보이면 자료에 무엇이
39
+ * 들어 있는지 알 수 있고, 빈칸이면 아무것도 알 수 없다.
40
+ */
41
+ export declare function deviceKindLabel(device: string | null | undefined, t: (k: string, o: {
42
+ defaultValue: string;
43
+ }) => string): string;
@@ -1,19 +1,62 @@
1
1
  /**
2
- * 기기 종류(보청기·인공와우·기타)에 붙는 배지 색.
2
+ * 기기 종류(보청기·인공와우·기타) — **이름과 색을 한 자리에서 정한다.**
3
3
  *
4
- * 표(DeviceSideTable)와 '사용중인 기기' 패널(PanelRowDevice)이 같은 값을 각자
5
- * 그리므로 **규칙을 한 곳에 둔다** — 두 곳에서 따로 정하면 같은 인공와우가 한쪽은
6
- * 파랑, 한쪽은 초록으로 떠서 다른 것인 줄 알게 된다.
4
+ * 표(`DeviceSideTable`)와 '사용중인 기기' 패널(`PanelRowDevice`)이 같은 값을 각자 그리므로
5
+ * 규칙을 한 곳에 둔다 — 두 곳에서 따로 정하면 같은 인공와우가 한쪽은 파랑, 한쪽은 초록으로
6
+ * 떠서 다른 것인 줄 알게 된다.
7
7
  *
8
- * 값은 저장된 그대로의 한국어다(managed_device.type). 목록에 없는 값·빈 값은
9
- * 'default'(무채색)로 떨어뜨린다 — 모르는 종류를 아는 색으로 칠하지 않는다.
8
+ * **색만 모여 있고 이름은 안 모여 있었다** — `deviceTypeKeys`·`deviceTypeLabel` 이 그 두
9
+ * 파일에 **주석까지 똑같이 두 벌** 있었다(2026-10-13 말귀가 짚었다). 색에 적어 둔 그 까닭이
10
+ * 이름에도 그대로 들어맞는 자리다. 모았다.
10
11
  *
11
- * @public
12
+ * ## 값은 **코드**다 — 옛 한글 값도 받는다
13
+ *
14
+ * 저장값(`managed_device.type`)이 `'보청기'` 에서 `'hearingAid'` 로 옮겨 가는 중이다
15
+ * (사장님 지시 "저장된 한글 값도 다 영어로"). 앱이 판을 돌리는 때와 이 판이 깔리는 때가
16
+ * 어긋나고, 옛 자료를 보는 다른 기관이 있을 수 있어 **둘 다 받는다**.
17
+ *
18
+ * 받지 않으면 **터지지 않고 조용히 틀린다** — 배지에 `hearingAid` 가 날것으로 뜨고
19
+ * (`defaultValue: device` 로 떨어진다), 색이 둘 다 무채색이 되어 종류를 색으로 가르던 것이
20
+ * 사라진다. 눈으로만 잡히는 꼴이다.
21
+ */
22
+ /** 저장값 → 코드. 옛 한글 값도 받는다(위 머리말) */
23
+ const TO_KIND = {
24
+ hearingAid: 'hearingAid', cochlearImplant: 'cochlearImplant', other: 'other',
25
+ // 열쇠가 **저장된 값**이라 따옴표를 쥐어 둔다 — 이름이 아니다(이름은 영어로 쓴다)
26
+ '보청기': 'hearingAid', '인공와우': 'cochlearImplant', '기타': 'other',
27
+ };
28
+ /**
29
+ * 저장값을 종류 코드로 — 모르는 값은 `null`.
30
+ *
31
+ * **모르는 것을 `'other'` 로 접지 않는다.** 기타는 "기타" 라고 고른 값이고, 모르는 값은
32
+ * 아직 뜻을 모르는 것이다. 둘을 섞으면 새 종류가 생긴 날 조용히 '기타' 로 보인다.
33
+ */
34
+ export function deviceKindOf(device) {
35
+ /* `device && …` 를 `?? null` 로 받으면 **빈 글자가 새어 나간다** — `'' && x` 는 `''` 이고
36
+ `?? ` 는 `null`·`undefined` 만 받으므로 `deviceKindOf('')` 가 `''` 를 돌려줬다(tsc 가 잡았다).
37
+ 빈 값은 "종류가 없다" 이지 "빈 이름의 종류" 가 아니다. */
38
+ if (!device)
39
+ return null;
40
+ return TO_KIND[device] ?? null;
41
+ }
42
+ /**
43
+ * 배지 색. 목록에 없는 값·빈 값은 `'default'`(무채색)로 떨어뜨린다 —
44
+ * **모르는 종류를 아는 색으로 칠하지 않는다.**
12
45
  */
13
46
  export function deviceKindVariant(device) {
14
- switch (device) {
15
- case '보청기': return 'main';
16
- case '인공와우': return 'sub';
47
+ switch (deviceKindOf(device)) {
48
+ case 'hearingAid': return 'main';
49
+ case 'cochlearImplant': return 'sub';
17
50
  default: return 'default';
18
51
  }
19
52
  }
53
+ /**
54
+ * 배지에 적을 이름 — **지금 언어로**. 사전(`device.type.*`)이 쥔다.
55
+ *
56
+ * 모르는 값은 **저장된 그대로** 보인다. 숨기지 않는다 — 화면에 날것이 보이면 자료에 무엇이
57
+ * 들어 있는지 알 수 있고, 빈칸이면 아무것도 알 수 없다.
58
+ */
59
+ export function deviceKindLabel(device, t) {
60
+ const kind = deviceKindOf(device);
61
+ return kind ? t(`device.type.${kind}`, { defaultValue: device ?? '' }) : (device ?? '');
62
+ }
@@ -6,10 +6,10 @@ import i18n from 'i18next';
6
6
  * 여기서 `../i18n` 을 가져오면 utils 묶음이 사전 JSON 과 init 부작용을 끌어안으므로 기본
7
7
  * 인스턴스를 직접 본다. 초기화 전의 `i18n.t` 는 undefined 를 돌려준다.
8
8
  */
9
- const 알수없는오류 = () => {
10
- const 기본 = '알 수 없는 오류';
11
- const s = typeof i18n?.t === 'function' ? i18n.t('common.unknownError', { defaultValue: 기본 }) : undefined;
12
- return typeof s === 'string' && s ? s : 기본;
9
+ const unknownError = () => {
10
+ const fallback = '알 수 없는 오류';
11
+ const s = typeof i18n?.t === 'function' ? i18n.t('common.unknownError', { defaultValue: fallback }) : undefined;
12
+ return typeof s === 'string' && s ? s : fallback;
13
13
  };
14
14
  /**
15
15
  * 잡은 값에서 **사람이 읽을 수 있는 원인**을 꺼낸다.
@@ -33,7 +33,7 @@ const 알수없는오류 = () => {
33
33
  export function getErrorMessage(e, fallback) {
34
34
  // 기본 문구는 부를 때마다 지금 언어로 만든다(모듈 로드 때 한 번 정하면 언어 전환을 못 따른다)
35
35
  if (fallback === undefined)
36
- fallback = 알수없는오류();
36
+ fallback = unknownError();
37
37
  if (e instanceof Error)
38
38
  return e.message || fallback;
39
39
  if (typeof e === 'string')
@@ -58,7 +58,7 @@ export function getErrorMessage(e, fallback) {
58
58
  export const NETWORK_ERROR_MESSAGE = '네트워크 연결이 불안정합니다. 연결 상태를 확인한 뒤 다시 시도해 주세요.';
59
59
  /* 화면에 내는 것은 지금 언어로(`common.networkError`, 사전에 없으면 위 한국어) — 상수만 돌려줘 영어 화면 토스트도 한국어였다.
60
60
  상수는 옛 소비자가 견주는 값이라 그대로 둔다. */
61
- const 네트워크오류 = () => {
61
+ const networkError = () => {
62
62
  const s = typeof i18n?.t === 'function' ? i18n.t('common.networkError', { defaultValue: NETWORK_ERROR_MESSAGE }) : undefined;
63
63
  return typeof s === 'string' && s ? s : NETWORK_ERROR_MESSAGE;
64
64
  };
@@ -82,5 +82,5 @@ export function isNetworkErrorMessage(msg) {
82
82
  * @public
83
83
  */
84
84
  export function toUserErrorMessage(msg) {
85
- return isNetworkErrorMessage(msg) ? 네트워크오류() : msg;
85
+ return isNetworkErrorMessage(msg) ? networkError() : msg;
86
86
  }
@@ -11,4 +11,4 @@ import type { FilterPanelSection } from '../types/filterPanel';
11
11
  * 안 담는 절: `note`(고르는 절이 아니다) · `input`(빈 칸을 '전체'라 말하면 거짓이다) ·
12
12
  * 고른 것이 없는데 '전체' 보기도 없는 절(말할 것이 없다).
13
13
  */
14
- export declare function buildFilterSummary(sections: readonly FilterPanelSection[], selected: Record<string, string | string[]> | undefined, 전체말: string): string;
14
+ export declare function buildFilterSummary(sections: readonly FilterPanelSection[], selected: Record<string, string | string[]> | undefined, allLabel: string): string;
@@ -11,36 +11,36 @@ import { FILTER_ALL_VALUE } from '../types/filterPanel';
11
11
  * 안 담는 절: `note`(고르는 절이 아니다) · `input`(빈 칸을 '전체'라 말하면 거짓이다) ·
12
12
  * 고른 것이 없는데 '전체' 보기도 없는 절(말할 것이 없다).
13
13
  */
14
- export function buildFilterSummary(sections, selected, 전체말) {
15
- const 조각들 = [];
14
+ export function buildFilterSummary(sections, selected, allLabel) {
15
+ const parts = [];
16
16
  for (const section of sections ?? []) {
17
17
  if (section.type === 'note' || section.type === 'input')
18
18
  continue;
19
- const 이름 = section.displayLabel ?? section.label;
20
- const 값 = 값말(section, selected?.[section.label], 전체말);
21
- if (값 === null)
19
+ const name = section.displayLabel ?? section.label;
20
+ const value = valueText(section, selected?.[section.label], allLabel);
21
+ if (value === null)
22
22
  continue;
23
- 조각들.push(`${이름} ${값}`);
23
+ parts.push(`${name} ${value}`);
24
24
  }
25
- return 조각들.join(' · ');
25
+ return parts.join(' · ');
26
26
  }
27
27
  /** 한 절이 무엇으로 정해져 있는지 — 말할 것이 없으면 `null` */
28
- function 값말(section, sel, 전체말) {
29
- const 고른것 = Array.isArray(sel) ? sel : sel === undefined ? [] : [sel];
28
+ function valueText(section, sel, allLabel) {
29
+ const picked = Array.isArray(sel) ? sel : sel === undefined ? [] : [sel];
30
30
  /* '전체' 는 두 꼴로 온다 — 아무것도 안 고른 것과 '전체' 보기를 고른 것. 같은 말로 읽는다. */
31
- const 실제 = 고른것.filter(v => v !== FILTER_ALL_VALUE);
32
- if (실제.length === 0) {
33
- return section.allOption ? (section.allLabel ?? 전체말) : null;
31
+ const actual = picked.filter(v => v !== FILTER_ALL_VALUE);
32
+ if (actual.length === 0) {
33
+ return section.allOption ? (section.allLabel ?? allLabel) : null;
34
34
  }
35
- return 실제.map(v => 보기말(section, v)).join(', ');
35
+ return actual.map(v => optionText(section, v)).join(', ');
36
36
  }
37
37
  /** 고른 값 하나를 사람이 읽는 말로 — 보기 목록에 없으면 값 그대로 */
38
- function 보기말(section, value) {
39
- const 보기들 = [
38
+ function optionText(section, value) {
39
+ const options = [
40
40
  ...(section.items ?? []),
41
41
  ...(section.groups ?? []).flat(),
42
42
  ];
43
- for (const item of 보기들) {
43
+ for (const item of options) {
44
44
  if (item == null)
45
45
  continue;
46
46
  if (typeof item === 'string') {
@@ -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
  }
@@ -1,6 +1,6 @@
1
1
  import type { RefObject } from 'react';
2
2
  /** 소비자가 통을 안 알려 줄 때 찾아 보는 이름 — 옛 동작 유지(말귀 셸의 본문). @internal */
3
- export declare const 기본통고르개 = ".content-area";
3
+ export declare const DEFAULT_SCROLL_SELECTOR = ".content-area";
4
4
  /**
5
5
  * 갈래를 고른 뒤 본문을 맨 위로 — 내비(SideNav·BottomNav)가 함께 쓴다. @internal
6
6
  *
@@ -8,4 +8,4 @@ export declare const 기본통고르개 = ".content-area";
8
8
  * 그것이 **아무 일도 하지 않는다** — 그때는 조용히 죽는 대신 개발 모드에 한 번 알린다.
9
9
  * 코어가 `.content-area`(소비자 클래스 이름)를 붙잡고 있던 자리라, 부르는 쪽이 통을 알려 주는 것이 정본이다.
10
10
  */
11
- export declare function scrollNavTargetToTop(scrollContainer?: string | RefObject<HTMLElement | null>, 어디서?: string): void;
11
+ export declare function scrollNavTargetToTop(scrollContainer?: string | RefObject<HTMLElement | null>, from?: string): void;
@@ -1,6 +1,6 @@
1
1
  import { devWarnOnce } from './devWarn';
2
2
  /** 소비자가 통을 안 알려 줄 때 찾아 보는 이름 — 옛 동작 유지(말귀 셸의 본문). @internal */
3
- export const 기본통고르개 = '.content-area';
3
+ export const DEFAULT_SCROLL_SELECTOR = '.content-area';
4
4
  /**
5
5
  * 갈래를 고른 뒤 본문을 맨 위로 — 내비(SideNav·BottomNav)가 함께 쓴다. @internal
6
6
  *
@@ -8,18 +8,18 @@ export const 기본통고르개 = '.content-area';
8
8
  * 그것이 **아무 일도 하지 않는다** — 그때는 조용히 죽는 대신 개발 모드에 한 번 알린다.
9
9
  * 코어가 `.content-area`(소비자 클래스 이름)를 붙잡고 있던 자리라, 부르는 쪽이 통을 알려 주는 것이 정본이다.
10
10
  */
11
- export function scrollNavTargetToTop(scrollContainer, 어디서 = 'Nav') {
11
+ export function scrollNavTargetToTop(scrollContainer, from = 'Nav') {
12
12
  try {
13
13
  const el = typeof scrollContainer === 'string'
14
14
  ? document.querySelector(scrollContainer)
15
- : scrollContainer?.current ?? document.querySelector(기본통고르개);
15
+ : scrollContainer?.current ?? document.querySelector(DEFAULT_SCROLL_SELECTOR);
16
16
  if (el) {
17
17
  el.scrollTop = 0;
18
18
  return;
19
19
  }
20
- const 문서 = document.scrollingElement ?? document.documentElement;
21
- if (문서 && 문서.scrollHeight <= 문서.clientHeight) {
22
- devWarnOnce('nav-scroll-container', `[${어디서}] 본문을 맨 위로 되돌릴 통을 못 찾았다(기본 ${기본통고르개}) — 창도 구를 수 없어 아무 일도 하지 않는다. scrollContainer 로 통을 알려 준다.`);
20
+ const doc = document.scrollingElement ?? document.documentElement;
21
+ if (doc && doc.scrollHeight <= doc.clientHeight) {
22
+ devWarnOnce('nav-scroll-container', `[${from}] 본문을 맨 위로 되돌릴 통을 못 찾았다(기본 ${DEFAULT_SCROLL_SELECTOR}) — 창도 구를 수 없어 아무 일도 하지 않는다. scrollContainer 로 통을 알려 준다.`);
23
23
  }
24
24
  window.scrollTo({ top: 0 });
25
25
  }
@@ -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