@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
@@ -21,7 +21,7 @@ export function getSavedTables() {
21
21
  }
22
22
  }
23
23
  /** 목록을 통째로 적는다. 저장소가 가득 찼거나(QuotaExceededError) 거부되면(사파리 프라이빗 모드) false. */
24
- function 적기(tables) {
24
+ function write(tables) {
25
25
  try {
26
26
  localStorage.setItem(STORAGE_KEY, JSON.stringify(tables));
27
27
  return true;
@@ -46,7 +46,7 @@ export function saveTable(name, items, description, searchConfig) {
46
46
  description,
47
47
  searchConfig,
48
48
  };
49
- return 적기([newTable, ...tables]) ? newTable : null;
49
+ return write([newTable, ...tables]) ? newTable : null;
50
50
  }
51
51
  /**
52
52
  * 저장된 단어표를 갱신한다(수정 저장). 지정한 필드만 덮어쓴다.
@@ -56,11 +56,11 @@ export function saveTable(name, items, description, searchConfig) {
56
56
  */
57
57
  export function updateSavedTable(id, patch) {
58
58
  const tables = getSavedTables().map(t => t.id === id ? { ...t, ...patch } : t);
59
- return 적기(tables);
59
+ return write(tables);
60
60
  }
61
61
  export function deleteTable(id) {
62
62
  const tables = getSavedTables().filter(t => t.id !== id);
63
- 적기(tables);
63
+ write(tables);
64
64
  }
65
65
  /** 표를 복사해 목록 맨 앞에 둔다. 원본이 없거나 **저장소에 적지 못하면 `null`**. */
66
66
  export function duplicateTable(id) {
@@ -75,5 +75,5 @@ export function duplicateTable(id) {
75
75
  name: `${original.name} (복사)`,
76
76
  savedAt: new Date().toISOString(),
77
77
  };
78
- return 적기([copy, ...tables]) ? copy : null;
78
+ return write([copy, ...tables]) ? copy : null;
79
79
  }
@@ -20,17 +20,17 @@ export interface TabSessionLike {
20
20
  * 여기서 막으면 이 탭이 로그인된 채로 남는다 — 막는 것보다 나쁜 쪽이다.
21
21
  *
22
22
  * ```ts
23
- * onAuthStateChange((event, 받은것) => {
23
+ * onAuthStateChange((event, incoming) => {
24
24
  * getSession().then(({ data }) => {
25
- * const 이탭것 = data.session
26
- * if (!isSessionForThisTab(받은것, 이탭것)) return // 다른 탭에서 온 것 — 무시
27
- * …이탭것 으로 상태를 맞춘다
25
+ * const thisTab = data.session
26
+ * if (!isSessionForThisTab(incoming, thisTab)) return // 다른 탭에서 온 것 — 무시
27
+ * …thisTab 으로 상태를 맞춘다
28
28
  * })
29
29
  * })
30
30
  * ```
31
31
  *
32
- * @param 받은것 이벤트가 실어 온 세션(없을 수 있다)
33
- * @param 이탭것 `getSession()` 이 이 탭 저장소에서 다시 읽은 세션
32
+ * @param incoming 이벤트가 실어 온 세션(없을 수 있다)
33
+ * @param thisTab `getSession()` 이 이 탭 저장소에서 다시 읽은 세션
34
34
  * @public
35
35
  */
36
- export declare function isSessionForThisTab(받은것: TabSessionLike | null | undefined, 이탭것: TabSessionLike | null | undefined): boolean;
36
+ export declare function isSessionForThisTab(incoming: TabSessionLike | null | undefined, thisTab: TabSessionLike | null | undefined): boolean;
@@ -16,21 +16,21 @@
16
16
  * 여기서 막으면 이 탭이 로그인된 채로 남는다 — 막는 것보다 나쁜 쪽이다.
17
17
  *
18
18
  * ```ts
19
- * onAuthStateChange((event, 받은것) => {
19
+ * onAuthStateChange((event, incoming) => {
20
20
  * getSession().then(({ data }) => {
21
- * const 이탭것 = data.session
22
- * if (!isSessionForThisTab(받은것, 이탭것)) return // 다른 탭에서 온 것 — 무시
23
- * …이탭것 으로 상태를 맞춘다
21
+ * const thisTab = data.session
22
+ * if (!isSessionForThisTab(incoming, thisTab)) return // 다른 탭에서 온 것 — 무시
23
+ * …thisTab 으로 상태를 맞춘다
24
24
  * })
25
25
  * })
26
26
  * ```
27
27
  *
28
- * @param 받은것 이벤트가 실어 온 세션(없을 수 있다)
29
- * @param 이탭것 `getSession()` 이 이 탭 저장소에서 다시 읽은 세션
28
+ * @param incoming 이벤트가 실어 온 세션(없을 수 있다)
29
+ * @param thisTab `getSession()` 이 이 탭 저장소에서 다시 읽은 세션
30
30
  * @public
31
31
  */
32
- export function isSessionForThisTab(받은것, 이탭것) {
33
- if (!받은것)
32
+ export function isSessionForThisTab(incoming, thisTab) {
33
+ if (!incoming)
34
34
  return true;
35
- return !!이탭것 && 이탭것.access_token === 받은것.access_token;
35
+ return !!thisTab && thisTab.access_token === incoming.access_token;
36
36
  }
@@ -1,11 +1,11 @@
1
1
  /* 목소리 목록을 기다리는 말 — 하나만 둔다. 전에는 기다리는 리스너가 부를 때마다 줄을 서고 stopSpeaking 이 치우지 못해,
2
2
  `speak('하나'); speak('둘'); stopSpeaking()` 뒤 목소리가 뜨는 순간 멈췄던 말이 모두 나왔다. */
3
- let 기다림 = null;
3
+ let pendingWait = null;
4
4
  /** voiceschanged 가 끝내 안 오는 환경(목소리 없음)에서도 말은 건다 — 안 그러면 onEnd 가 영영 안 불렸다 */
5
- const 목소리기다림MS = 1500;
6
- function 기다림치우기() {
7
- 기다림?.치우기();
8
- 기다림 = null;
5
+ const VOICE_WAIT_MS = 1500;
6
+ function clearPendingWait() {
7
+ pendingWait?.clear();
8
+ pendingWait = null;
9
9
  }
10
10
  /** @public */
11
11
  export function speak(text, { onEnd, rate = 0.9 } = {}) {
@@ -14,9 +14,9 @@ export function speak(text, { onEnd, rate = 0.9 } = {}) {
14
14
  return;
15
15
  }
16
16
  const synth = window.speechSynthesis;
17
- 기다림치우기();
17
+ clearPendingWait();
18
18
  const doSpeak = () => {
19
- 기다림치우기();
19
+ clearPendingWait();
20
20
  synth.cancel();
21
21
  const u = new SpeechSynthesisUtterance(text);
22
22
  const voices = synth.getVoices();
@@ -47,18 +47,18 @@ export function speak(text, { onEnd, rate = 0.9 } = {}) {
47
47
  doSpeak();
48
48
  return;
49
49
  }
50
- const 시계 = setTimeout(doSpeak, 목소리기다림MS);
50
+ const timer = setTimeout(doSpeak, VOICE_WAIT_MS);
51
51
  synth.addEventListener('voiceschanged', doSpeak, { once: true });
52
- 기다림 = {
53
- 치우기: () => {
54
- clearTimeout(시계);
52
+ pendingWait = {
53
+ clear: () => {
54
+ clearTimeout(timer);
55
55
  synth.removeEventListener('voiceschanged', doSpeak);
56
56
  },
57
57
  };
58
58
  }
59
59
  /** @public */
60
60
  export function stopSpeaking() {
61
- 기다림치우기();
61
+ clearPendingWait();
62
62
  if (typeof window !== 'undefined' && 'speechSynthesis' in window)
63
63
  window.speechSynthesis.cancel();
64
64
  }
@@ -1,5 +1,5 @@
1
1
  /** 글자 둘을 한국어 차례로 견준다 */
2
- export declare function 한글순(a: string, b: string): number;
2
+ export declare function compareKorean(a: string, b: string): number;
3
3
  /**
4
4
  * 표의 한 칸끼리 견준다 — **숫자는 숫자로, 글자 속 숫자도 수로.**
5
5
  *
@@ -13,10 +13,10 @@ export declare function 한글순(a: string, b: string): number;
13
13
  * 비어 있는 값(`null`·`undefined`·NaN)은 빈 글자로 본다 — 부르는 쪽이 이미
14
14
  * `?? ''` 로 채워 넘기던 것과 같은 뜻이다.
15
15
  */
16
- export declare function 칸순(a: unknown, b: unknown): number;
16
+ export declare function compareCell(a: unknown, b: unknown): number;
17
17
  /**
18
- * 표 정렬 한 번의 비교 — 칸순에 방향을 입히되 **빈 값은 오름·내림 모두 맨 뒤.**
19
- * 방향을 `-칸순` 으로 뒤집기만 하면 빈 값이 오름차순 맨 앞·내림차순 맨 뒤로 오가, 표마다 빈 줄이 다른 끝에 섰다.
18
+ * 표 정렬 한 번의 비교 — compareCell에 방향을 입히되 **빈 값은 오름·내림 모두 맨 뒤.**
19
+ * 방향을 `-compareCell` 으로 뒤집기만 하면 빈 값이 오름차순 맨 앞·내림차순 맨 뒤로 오가, 표마다 빈 줄이 다른 끝에 섰다.
20
20
  * ListPopup 과 useTableSort().sortRows 가 함께 쓴다.
21
21
  */
22
- export declare function 칸순빈값뒤(a: unknown, b: unknown, asc: boolean): number;
22
+ export declare function compareCellBlankLast(a: unknown, b: unknown, asc: boolean): number;
@@ -10,44 +10,44 @@
10
10
  *
11
11
  * 콜레이터는 만드는 값이 비싸다 — 한 번만 만들어 쓴다.
12
12
  */
13
- const 콜레이터 = (() => {
14
- let 것 = null;
13
+ const collator = (() => {
14
+ let it = null;
15
15
  return () => {
16
- if (!것)
17
- 것 = new Intl.Collator('ko');
18
- return 것;
16
+ if (!it)
17
+ it = new Intl.Collator('ko');
18
+ return it;
19
19
  };
20
20
  })();
21
21
  /** 글자 둘을 한국어 차례로 견준다 */
22
- export function 한글순(a, b) {
23
- return 콜레이터().compare(a, b);
22
+ export function compareKorean(a, b) {
23
+ return collator().compare(a, b);
24
24
  }
25
- const 조각열쇠 = new Map();
26
- function 조각내기(s) {
27
- const 있음 = 조각열쇠.get(s);
28
- if (있음)
29
- return 있음;
25
+ const pieceCache = new Map();
26
+ function toPieces(s) {
27
+ const hit = pieceCache.get(s);
28
+ if (hit)
29
+ return hit;
30
30
  const out = [];
31
31
  // 뒤돌아보기(?<=) 는 iOS 15 Safari 가 못 읽어 모듈째 깨지므로 쓰지 않는다 — 빼기표 앞 글자를 손으로 본다
32
32
  const re = /-?\d+(?:\.\d+)?/g;
33
- let 끝 = 0;
33
+ let end = 0;
34
34
  for (let m = re.exec(s); m; m = re.exec(s)) {
35
- let 시작 = m.index;
36
- let 수 = m[0];
37
- if (수[0] === '-' && 시작 > 0 && !/\s/.test(s[시작 - 1])) {
38
- 시작 += 1;
39
- 수 = 수.slice(1);
35
+ let start = m.index;
36
+ let numText = m[0];
37
+ if (numText[0] === '-' && start > 0 && !/\s/.test(s[start - 1])) {
38
+ start += 1;
39
+ numText = numText.slice(1);
40
40
  }
41
- if (시작 > 끝)
42
- out.push(s.slice(끝, 시작));
43
- out.push(Number(수));
44
- 끝 = m.index + m[0].length;
41
+ if (start > end)
42
+ out.push(s.slice(end, start));
43
+ out.push(Number(numText));
44
+ end = m.index + m[0].length;
45
45
  }
46
- if (끝 < s.length)
47
- out.push(s.slice(끝));
48
- if (조각열쇠.size > 5000)
49
- 조각열쇠.clear();
50
- 조각열쇠.set(s, out);
46
+ if (end < s.length)
47
+ out.push(s.slice(end));
48
+ if (pieceCache.size > 5000)
49
+ pieceCache.clear();
50
+ pieceCache.set(s, out);
51
51
  return out;
52
52
  }
53
53
  /* **전화번호 꼴**(숫자·하이픈·빈칸만, 숫자 9–11자리, 0 으로 시작 — 1 로 시작하면 하이픈·빈칸이 있을 때만)은 숫자만 뽑은 **글자 조각 하나**가 열쇠다.
@@ -55,14 +55,14 @@ function 조각내기(s) {
55
55
  '010-…' 뒤에 서서, 같은 번호가 하이픈 유무로 표의 다른 자리로 갔다.
56
56
  두 값이 모두 전화 꼴이면 숫자 글자끼리 견주는 셈이 된다. 한쪽만 전화 꼴일 때도 **값마다 열쇠가 하나로 정해져** 있어야
57
57
  차례가 돌지 않는다(값 쌍마다 규칙을 고르면 A<B<C<A 가 생긴다) — 글자 조각이라 수 조각 뒤, 다른 글자와는 한국어 차례. */
58
- function 전화열쇠(s) {
58
+ function phoneKey(s) {
59
59
  if (!/^[\d\s-]+$/.test(s))
60
60
  return null;
61
- const 숫자 = s.replace(/\D/g, '');
62
- if (숫자.length < 9 || 숫자.length > 11)
61
+ const digits = s.replace(/\D/g, '');
62
+ if (digits.length < 9 || digits.length > 11)
63
63
  return null;
64
64
  // 0 으로 시작하면 전화. 1 로 시작하면 하이픈·빈칸이 있을 때만 — 하이픈 없는 '1234567890' 같은 번호 ID 는 수로 둔다
65
- return 숫자[0] === '0' || (숫자[0] === '1' && s !== 숫자) ? 숫자 : null;
65
+ return digits[0] === '0' || (digits[0] === '1' && s !== digits) ? digits : null;
66
66
  }
67
67
  /**
68
68
  * 표의 한 칸끼리 견준다 — **숫자는 숫자로, 글자 속 숫자도 수로.**
@@ -77,19 +77,19 @@ function 전화열쇠(s) {
77
77
  * 비어 있는 값(`null`·`undefined`·NaN)은 빈 글자로 본다 — 부르는 쪽이 이미
78
78
  * `?? ''` 로 채워 넘기던 것과 같은 뜻이다.
79
79
  */
80
- export function 칸순(a, b) {
81
- const 글 = (v) => (v == null || (typeof v === 'number' && Number.isNaN(v)) ? '' : String(v).trim());
82
- const sa = 글(a), sb = 글(b);
80
+ export function compareCell(a, b) {
81
+ const text = (v) => (v == null || (typeof v === 'number' && Number.isNaN(v)) ? '' : String(v).trim());
82
+ const sa = text(a), sb = text(b);
83
83
  if (sa === sb)
84
84
  return 0;
85
85
  /* 수(number)는 글로 바꿔 쪼개지 않고 **수 조각 하나** 로 — '1e+21'·'1e-7' 로 쪼개면 1e21 이 5 앞에, -Infinity 가 끝에 섰다 */
86
- const 열쇠 = (v, s) => {
86
+ const keyOf = (v, s) => {
87
87
  if (typeof v === 'number' && !Number.isNaN(v))
88
88
  return [v];
89
- const 전화 = 전화열쇠(s);
90
- return 전화 !== null ? [전화] : 조각내기(s);
89
+ const phone = phoneKey(s);
90
+ return phone !== null ? [phone] : toPieces(s);
91
91
  };
92
- const ka = 열쇠(a, sa), kb = 열쇠(b, sb);
92
+ const ka = keyOf(a, sa), kb = keyOf(b, sb);
93
93
  const n = Math.min(ka.length, kb.length);
94
94
  for (let i = 0; i < n; i++) {
95
95
  const x = ka[i], y = kb[i];
@@ -102,25 +102,25 @@ export function 칸순(a, b) {
102
102
  return -1;
103
103
  if (typeof y === 'number')
104
104
  return 1;
105
- const c = 한글순(x, y);
105
+ const c = compareKorean(x, y);
106
106
  if (c !== 0)
107
107
  return c;
108
108
  }
109
109
  if (ka.length !== kb.length)
110
110
  return ka.length - kb.length;
111
- return 한글순(sa, sb);
111
+ return compareKorean(sa, sb);
112
112
  }
113
- /* 빈 값 — null·undefined·NaN·빈 글자(빈칸만 있는 글자 포함). 칸순이 '' 로 보는 것과 같은 범위. */
114
- const 빈칸값 = (v) => v == null || (typeof v === 'number' && Number.isNaN(v)) || (typeof v === 'string' && v.trim() === '');
113
+ /* 빈 값 — null·undefined·NaN·빈 글자(빈칸만 있는 글자 포함). compareCell이 '' 로 보는 것과 같은 범위. */
114
+ const isBlank = (v) => v == null || (typeof v === 'number' && Number.isNaN(v)) || (typeof v === 'string' && v.trim() === '');
115
115
  /**
116
- * 표 정렬 한 번의 비교 — 칸순에 방향을 입히되 **빈 값은 오름·내림 모두 맨 뒤.**
117
- * 방향을 `-칸순` 으로 뒤집기만 하면 빈 값이 오름차순 맨 앞·내림차순 맨 뒤로 오가, 표마다 빈 줄이 다른 끝에 섰다.
116
+ * 표 정렬 한 번의 비교 — compareCell에 방향을 입히되 **빈 값은 오름·내림 모두 맨 뒤.**
117
+ * 방향을 `-compareCell` 으로 뒤집기만 하면 빈 값이 오름차순 맨 앞·내림차순 맨 뒤로 오가, 표마다 빈 줄이 다른 끝에 섰다.
118
118
  * ListPopup 과 useTableSort().sortRows 가 함께 쓴다.
119
119
  */
120
- export function 칸순빈값뒤(a, b, asc) {
121
- const ea = 빈칸값(a), eb = 빈칸값(b);
120
+ export function compareCellBlankLast(a, b, asc) {
121
+ const ea = isBlank(a), eb = isBlank(b);
122
122
  if (ea || eb)
123
123
  return ea === eb ? 0 : ea ? 1 : -1;
124
- const c = 칸순(a, b);
124
+ const c = compareCell(a, b);
125
125
  return asc ? c : -c;
126
126
  }
@@ -18,10 +18,10 @@ export function getSavedTheme() {
18
18
  export function applyTheme(mode) {
19
19
  /* 세 값만 받는다 — 설정은 서버(JSON)에서 오기도 해서 타입만 믿고 그대로 박으면, 이 속성을 글자로 옮겨 짓는 인쇄 문서에
20
20
  `"><script>` 가 끼어들었다. 모르는 값은 기본(light)으로. */
21
- const 값 = mode === 'light' || mode === 'dark' || mode === 'auto' ? mode : DEFAULT_THEME;
21
+ const value = mode === 'light' || mode === 'dark' || mode === 'auto' ? mode : DEFAULT_THEME;
22
22
  try {
23
- document.documentElement.setAttribute('data-theme', 값);
24
- localStorage.setItem(STORAGE_KEY, 값);
23
+ document.documentElement.setAttribute('data-theme', value);
24
+ localStorage.setItem(STORAGE_KEY, value);
25
25
  }
26
26
  catch { }
27
27
  }
@@ -24,7 +24,7 @@
24
24
  * 여기서 멋대로 UTC 로 돌리면 그 뜻이 깎인다. 읽히는지만 보고 **그대로** 둔다.
25
25
  * `Date`·숫자(epoch)로 오면 그때는 쓸 글자가 없으니 ISO 로 짓는다.
26
26
  */
27
- function 때(v) {
27
+ function timeOf(v) {
28
28
  if (typeof v === 'string') {
29
29
  const s = v.trim();
30
30
  if (!s)
@@ -87,8 +87,8 @@ export function toItemResults(raw) {
87
87
  correct: itemCorrectness(r),
88
88
  /* 칸 이름은 **camelCase 와 snake_case 둘 다** 본다 — 화면이 서버 꼴(`shown_at`)을 그대로
89
89
  들고 있는 자리가 있다. 이름이 하나라고 믿었다가 조용히 떨어뜨리는 것이 이 청의 뿌리다. */
90
- shownAt: 때(r.shownAt) ?? 때(r.shown_at),
91
- answeredAt: 때(r.answeredAt) ?? 때(r.answered_at),
90
+ shownAt: timeOf(r.shownAt) ?? timeOf(r.shown_at),
91
+ answeredAt: timeOf(r.answeredAt) ?? timeOf(r.answered_at),
92
92
  };
93
93
  });
94
94
  }
@@ -80,13 +80,13 @@ export async function checkAudioExists(word) {
80
80
  // 한 번의 통신 끊김·서버 장애가 그 단어를 세션 내내 '음성 없음'으로 고정해, '음성 있음' 필터에서
81
81
  // 통째로 빠진다. 실패는 지워서 다음 호출이 다시 물어보게 한다 — 단 **이 요청이 담은 칸일 때만**:
82
82
  // clearAudioExistsCache() 뒤 새로 담긴 확인을 늦게 끝난 옛 요청이 지우면 같은 주소를 또 묻는다.
83
- const 확인안됨 = () => {
83
+ const onUnconfirmed = () => {
84
84
  if (audioExistsCache.get(url) === promise)
85
85
  audioExistsCache.delete(url);
86
86
  return false;
87
87
  };
88
88
  const promise = fetch(url, { method: 'HEAD', signal: timeoutSignal(8000) })
89
- .then(res => (res.status >= 500 ? 확인안됨() : res.ok), 확인안됨);
89
+ .then(res => (res.status >= 500 ? onUnconfirmed() : res.ok), onUnconfirmed);
90
90
  audioExistsCache.set(url, promise);
91
91
  return promise;
92
92
  }
@@ -10,7 +10,7 @@ export type WordAttributeSectionCode = 'grade' | 'syllables' | 'pos' | 'wordType
10
10
  * (`undefined` · `null` · `''`, 그리고 공백만 든 것도).
11
11
  *
12
12
  * 그래서 **맞추개가 이 코드만은 다른 길로 간다** — `word.grade === code` 가 아니라
13
- * `빈등급인가(word.grade)` 로 본다(`phonemePairSearch.matchesFilters`). 같은 꼴로 재면
13
+ * `isBlankGrade(word.grade)` 로 본다(`phonemePairSearch.matchesFilters`). 같은 꼴로 재면
14
14
  * 한 단어도 안 걸리고, 그런데 **필터는 멀쩡히 켜져 보인다.**
15
15
  *
16
16
  * **숫자 길에도 안 걸린다** — 아래 `wordAttributeValueCodeOf` 가 등급을 숫자로도 읽는데
@@ -20,7 +20,7 @@ export type WordAttributeSectionCode = 'grade' | 'syllables' | 'pos' | 'wordType
20
20
  */
21
21
  export declare const GRADE_NONE = "none";
22
22
  /** 등급이 비었나 — `'none'` 이 가리키는 그 **빔**을 한 자리에서 판정한다. 공백만 든 것도 빔이다. */
23
- export declare function 빈등급인가(grade: string | null | undefined): boolean;
23
+ export declare function isBlankGrade(grade: string | null | undefined): boolean;
24
24
  /** 등급 코드인가 — `'1'`~`'4'`·`'none'`. 저장된 값을 되읽을 때 **아닌 것은 버린다**(한때 `''` 가 저장됐다). */
25
25
  export declare function isGradeCode(v: unknown): v is string;
26
26
  /**
@@ -21,7 +21,7 @@ import i18n from '../i18n';
21
21
  * (`undefined` · `null` · `''`, 그리고 공백만 든 것도).
22
22
  *
23
23
  * 그래서 **맞추개가 이 코드만은 다른 길로 간다** — `word.grade === code` 가 아니라
24
- * `빈등급인가(word.grade)` 로 본다(`phonemePairSearch.matchesFilters`). 같은 꼴로 재면
24
+ * `isBlankGrade(word.grade)` 로 본다(`phonemePairSearch.matchesFilters`). 같은 꼴로 재면
25
25
  * 한 단어도 안 걸리고, 그런데 **필터는 멀쩡히 켜져 보인다.**
26
26
  *
27
27
  * **숫자 길에도 안 걸린다** — 아래 `wordAttributeValueCodeOf` 가 등급을 숫자로도 읽는데
@@ -31,12 +31,12 @@ import i18n from '../i18n';
31
31
  */
32
32
  export const GRADE_NONE = 'none';
33
33
  /** 등급이 비었나 — `'none'` 이 가리키는 그 **빔**을 한 자리에서 판정한다. 공백만 든 것도 빔이다. */
34
- export function 빈등급인가(grade) {
34
+ export function isBlankGrade(grade) {
35
35
  return grade == null || String(grade).trim() === '';
36
36
  }
37
37
  /** 등급 코드인가 — `'1'`~`'4'`·`'none'`. 저장된 값을 되읽을 때 **아닌 것은 버린다**(한때 `''` 가 저장됐다). */
38
38
  export function isGradeCode(v) {
39
- return typeof v === 'string' && 등급값.includes(v);
39
+ return typeof v === 'string' && gradeValues.includes(v);
40
40
  }
41
41
  /**
42
42
  * **등급 코드 집합으로 단어 하나를 거른다 — 이 결정은 여기 하나다.**
@@ -55,46 +55,46 @@ export function matchesGradeCodes(codes, grade) {
55
55
  const set = codes instanceof Set ? codes : new Set(codes);
56
56
  if (set.size === 0)
57
57
  return true;
58
- if (빈등급인가(grade))
58
+ if (isBlankGrade(grade))
59
59
  return set.has(GRADE_NONE);
60
60
  return set.has(String(grade));
61
61
  }
62
- const 섹션들 = ['grade', 'syllables', 'pos', 'wordType'];
62
+ const sectionCodes = ['grade', 'syllables', 'pos', 'wordType'];
63
63
  /** 등급 코드 — `'none'` 은 **등급이 빈 단어**다(값이 아니라 **빔**이다. GRADE_NONE 주석을 볼 것) */
64
- const 등급값 = ['1', '2', '3', '4', 'none'];
65
- const 음절값 = ['1', '2', '3', '4', '5+'];
66
- const 품사값 = ['noun', 'verb', 'modifier'];
67
- const 어종값 = ['native', 'sino', 'foreign', 'mixed'];
68
- const 코드값 = { grade: 등급값, syllables: 음절값, pos: 품사값, wordType: 어종값 };
64
+ const gradeValues = ['1', '2', '3', '4', 'none'];
65
+ const syllableValues = ['1', '2', '3', '4', '5+'];
66
+ const posValues = ['noun', 'verb', 'modifier'];
67
+ const wordTypeValues = ['native', 'sino', 'foreign', 'mixed'];
68
+ const valuesBySection = { grade: gradeValues, syllables: syllableValues, pos: posValues, wordType: wordTypeValues };
69
69
  /** 품사 코드 → 사전 품사 약자(단어의 pos 에 들어 있는지 본다) · 어종 코드 → 사전 어종 값 */
70
70
  export const POS_CODE_TARGETS = { noun: ['명', '수'], verb: ['동', '형'], modifier: ['부', '관'] };
71
71
  export const WORD_TYPE_CODE_VALUES = { native: '고유', sino: '한자', foreign: '외래', mixed: '혼종' };
72
72
  /* 사전 밖의 옛 라벨 — phonemePairSearch 의 DEFAULT_PAIR_FILTER_KEYS 가 쓰던 것(쉼표 뒤 빈칸 없음·'난이도 등급') */
73
- const 옛섹션 = { grade: ['난이도 등급'] };
74
- const 옛값 = {
73
+ const legacySectionLabels = { grade: ['난이도 등급'] };
74
+ const legacyValueLabels = {
75
75
  pos: { noun: ['체언(명사,수사)'], verb: ['용언(동사,형용사)'], modifier: ['수식언(부사,관형사)'] },
76
76
  };
77
77
  /* 언어마다 한 번 만든다 — matchesFilters 는 낱말(2만여 개)마다 불린다. 사전이 더해지면(addResourceBundle) 비운다 */
78
- const 사전들 = new Map();
78
+ const dictCache = new Map();
79
79
  try {
80
- i18n.store?.on?.('added', () => 사전들.clear());
80
+ i18n.store?.on?.('added', () => dictCache.clear());
81
81
  }
82
82
  catch { /* 없는 환경 */ }
83
- function 사전(lng) {
84
- const 있음 = 사전들.get(lng);
85
- if (있음)
86
- return 있음;
83
+ function dictFor(lng) {
84
+ const hit = dictCache.get(lng);
85
+ if (hit)
86
+ return hit;
87
87
  const t = i18n.getFixedT(lng);
88
88
  const d = {
89
- 섹션: {
89
+ section: {
90
90
  grade: t('wordSearch.filter.difficulty', '등급'),
91
91
  syllables: t('wordSearch.filter.syllables', '음절 수'),
92
92
  pos: t('wordSearch.filter.partOfSpeech', '품사'),
93
93
  wordType: t('wordSearch.filter.wordType', '유형'),
94
94
  },
95
- 값: {
95
+ value: {
96
96
  grade: {
97
- ...Object.fromEntries(등급값.filter(n => n !== GRADE_NONE).map((n) => [n, t('wordSearch.filter.grade', { n: Number(n), defaultValue: `${n}등급` })])),
97
+ ...Object.fromEntries(gradeValues.filter(n => n !== GRADE_NONE).map((n) => [n, t('wordSearch.filter.grade', { n: Number(n), defaultValue: `${n}등급` })])),
98
98
  /* **`없음` 은 숫자 틀을 안 쓴다** — `t('…grade', { n: Number('none') })` 은 `NaN등급` 이 된다 */
99
99
  [GRADE_NONE]: t('wordSearch.filter.gradeNone', '없음'),
100
100
  },
@@ -115,36 +115,36 @@ function 사전(lng) {
115
115
  },
116
116
  },
117
117
  };
118
- 사전들.set(lng, d);
118
+ dictCache.set(lng, d);
119
119
  return d;
120
120
  }
121
- const 언어들 = () => Array.from(new Set([i18n.language, 'ko', 'en'].filter((x) => !!x)));
121
+ const languages = () => Array.from(new Set([i18n.language, 'ko', 'en'].filter((x) => !!x)));
122
122
  /** 섹션 라벨(어느 언어든·옛 라벨·코드) → 섹션 코드. 속성 섹션이 아니면 null. */
123
123
  export function wordAttributeSectionOf(label) {
124
- if (섹션들.includes(label))
124
+ if (sectionCodes.includes(label))
125
125
  return label;
126
- for (const lng of 언어들()) {
127
- const d = 사전(lng);
128
- for (const s of 섹션들)
129
- if (d.섹션[s] === label)
126
+ for (const lng of languages()) {
127
+ const d = dictFor(lng);
128
+ for (const s of sectionCodes)
129
+ if (d.section[s] === label)
130
130
  return s;
131
131
  }
132
- for (const s of 섹션들)
133
- if (옛섹션[s]?.includes(label))
132
+ for (const s of sectionCodes)
133
+ if (legacySectionLabels[s]?.includes(label))
134
134
  return s;
135
135
  return null;
136
136
  }
137
137
  /** 한 섹션의 값(코드·어느 언어 라벨·옛 라벨) → 값 코드. 못 읽으면 null. */
138
138
  export function wordAttributeValueCodeOf(section, value) {
139
- if (코드값[section].includes(value))
139
+ if (valuesBySection[section].includes(value))
140
140
  return value;
141
- for (const lng of 언어들()) {
142
- const 표 = 사전(lng).값[section];
143
- for (const [code, label] of Object.entries(표))
141
+ for (const lng of languages()) {
142
+ const table = dictFor(lng).value[section];
143
+ for (const [code, label] of Object.entries(table))
144
144
  if (label === value)
145
145
  return code;
146
146
  }
147
- for (const [code, labels] of Object.entries(옛값[section] ?? {}))
147
+ for (const [code, labels] of Object.entries(legacyValueLabels[section] ?? {}))
148
148
  if (labels.includes(value))
149
149
  return code;
150
150
  /* 등급·음절은 숫자로도 읽는다('Grade 3'·'3등급'·'6음절') — 사전에 없는 꼴이 와도 풀리지 않게 */
@@ -160,9 +160,9 @@ export function wordAttributeValueCodeOf(section, value) {
160
160
  }
161
161
  /** 코드 → 지금 언어의 라벨. `'none'` 도 사전 꼴로 나온다(숫자 틀 `{{n}}등급` 에 넣으면 `NaN등급` 이 된다). 모르는 코드는 그대로. */
162
162
  export function wordAttributeValueLabelOf(section, code, lng = i18n.language) {
163
- return 사전(lng).값[section][code] ?? code;
163
+ return dictFor(lng).value[section][code] ?? code;
164
164
  }
165
- const 배열로 = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string') : typeof v === 'string' && v ? [v] : []);
165
+ const toArray = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string') : typeof v === 'string' && v ? [v] : []);
166
166
  /**
167
167
  * @public filterSelected(라벨 열쇠·라벨 값, 어느 언어든 섞여 있어도) → **코드 모양**. 저장할 때 쓴다.
168
168
  * 속성 섹션(등급·음절 수·품사·어종)만 담고, 그 밖의 섹션(초성·선택·단어 입력 등)은 뺀다. 못 읽는 값은 버린다.
@@ -173,7 +173,7 @@ export function toWordAttributeFilterCodes(selected) {
173
173
  const s = wordAttributeSectionOf(k);
174
174
  if (!s)
175
175
  continue;
176
- for (const raw of 배열로(v)) {
176
+ for (const raw of toArray(v)) {
177
177
  const c = wordAttributeValueCodeOf(s, raw);
178
178
  if (!c)
179
179
  continue;
@@ -188,12 +188,12 @@ export function toWordAttributeFilterCodes(selected) {
188
188
  * @public 코드 모양 → 지정한 언어(기본: 지금 i18n 언어)의 **라벨 filterSelected**. FilterPanel 의 selected 로 그대로 쓴다.
189
189
  */
190
190
  export function wordAttributeFilterFromCodes(codes, lng = i18n.language) {
191
- const d = 사전(lng || 'ko');
191
+ const d = dictFor(lng || 'ko');
192
192
  const out = {};
193
- for (const s of 섹션들) {
194
- const labels = (codes?.[s] ?? []).map((c) => d.값[s][c]).filter((x) => !!x);
193
+ for (const s of sectionCodes) {
194
+ const labels = (codes?.[s] ?? []).map((c) => d.value[s][c]).filter((x) => !!x);
195
195
  if (labels.length)
196
- out[d.섹션[s]] = labels;
196
+ out[d.section[s]] = labels;
197
197
  }
198
198
  return out;
199
199
  }
@@ -203,17 +203,17 @@ export function wordAttributeFilterFromCodes(codes, lng = i18n.language) {
203
203
  * 들어 있으면 합친다.
204
204
  */
205
205
  export function normalizeWordAttributeFilter(selected, lng = i18n.language) {
206
- const 남김 = {};
206
+ const kept = {};
207
207
  for (const [k, v] of Object.entries(selected ?? {}))
208
208
  if (!wordAttributeSectionOf(k))
209
- 남김[k] = v;
210
- return { ...남김, ...wordAttributeFilterFromCodes(toWordAttributeFilterCodes(selected), lng) };
209
+ kept[k] = v;
210
+ return { ...kept, ...wordAttributeFilterFromCodes(toWordAttributeFilterCodes(selected), lng) };
211
211
  }
212
212
  /** 라벨이 어느 언어의 섹션 라벨인가 — 토글이 그 언어로 값을 적게 */
213
213
  export function wordAttributeLabelLanguageOf(label) {
214
- for (const lng of 언어들()) {
215
- const d = 사전(lng);
216
- if (섹션들.some((s) => d.섹션[s] === label))
214
+ for (const lng of languages()) {
215
+ const d = dictFor(lng);
216
+ if (sectionCodes.some((s) => d.section[s] === label))
217
217
  return lng;
218
218
  }
219
219
  return i18n.language || 'ko';
@@ -147,10 +147,10 @@ position) {
147
147
  else if (position === 'tail')
148
148
  i = matchesAt(pw, L - 1) ? L - 1 : -1;
149
149
  else if (position === 'mid') {
150
- const 처음 = selParts.includes('cho') ? 1 : 0; // 첫 초성은 어두다
151
- const 마지막 = selParts.includes('jong') ? L - 2 : L - 1; // 마지막 받침은 어말이다
150
+ const first = selParts.includes('cho') ? 1 : 0; // 첫 초성은 어두다
151
+ const last = selParts.includes('jong') ? L - 2 : L - 1; // 마지막 받침은 어말이다
152
152
  i = -1;
153
- for (let k = 처음; k <= 마지막; k++)
153
+ for (let k = first; k <= last; k++)
154
154
  if (matchesAt(pw, k)) {
155
155
  i = k;
156
156
  break;
@@ -145,7 +145,7 @@ async function buildIndexAsync(items, onProgress) {
145
145
  return result;
146
146
  }
147
147
  /** 캐시를 비우거나 새 빌드를 시작할 때 오른다 — 늦게 끝난 옛 빌드가 캐시에 쓰지 못하게 */
148
- let 빌드세대 = 0;
148
+ let buildGeneration = 0;
149
149
  /**
150
150
  * @public
151
151
  * 단어 리스트로부터 대립쌍 인덱스를 빌드 (또는 캐시된 결과 반환).
@@ -166,9 +166,9 @@ export async function buildPairIndex(items, options = {}) {
166
166
  }
167
167
  /* 세대가 그대로일 때만 캐시에 담는다 — 빌드 중 clearPairIndexCache() 가 불렸거나 더 새 빌드가 시작됐으면, 늦게 끝난 옛
168
168
  빌드가 방금 지운 캐시를 되살리거나 새 인덱스를 옛 것으로 덮었다. 부른 쪽에는 만든 것을 그대로 돌려준다. */
169
- const 내세대 = ++빌드세대;
169
+ const myGeneration = ++buildGeneration;
170
170
  const built = await buildIndexAsync(items, options.onProgress);
171
- if (내세대 === 빌드세대) {
171
+ if (myGeneration === buildGeneration) {
172
172
  memIndex = built;
173
173
  memSig = sig;
174
174
  saveToLocalStorage(built, sig);
@@ -189,7 +189,7 @@ export function getPairIndex() {
189
189
  }
190
190
  /** @public 캐시 초기화 */
191
191
  export function clearPairIndexCache() {
192
- 빌드세대 += 1;
192
+ buildGeneration += 1;
193
193
  memIndex = null;
194
194
  memSig = null;
195
195
  try {