@ihabdevteam/core 0.149.0 → 0.151.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 (219) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/components.cjs.js +199 -186
  3. package/dist/components.esm.css +5 -5
  4. package/dist/hooks.cjs.js +5 -1
  5. package/dist/i18n.cjs.js +3 -3
  6. package/dist/ihabdevteam-core-tokens.css +1 -1
  7. package/dist/ihabdevteam-core.cjs.js +200 -187
  8. package/dist/ihabdevteam-core.css +1 -1
  9. package/dist/ihabdevteam-core.d.ts +407 -69
  10. package/dist/packages/core/src/components/Accordion.d.ts +6 -0
  11. package/dist/packages/core/src/components/Accordion.js +5 -2
  12. package/dist/packages/core/src/components/Alert.d.ts +7 -1
  13. package/dist/packages/core/src/components/Alert.js +12 -3
  14. package/dist/packages/core/src/components/BarChart.js +7 -2
  15. package/dist/packages/core/src/components/Board.d.ts +5 -2
  16. package/dist/packages/core/src/components/Board.js +9 -3
  17. package/dist/packages/core/src/components/BottomNav.js +5 -4
  18. package/dist/packages/core/src/components/Breadcrumb.css +9 -0
  19. package/dist/packages/core/src/components/Breadcrumb.d.ts +12 -1
  20. package/dist/packages/core/src/components/Breadcrumb.js +8 -2
  21. package/dist/packages/core/src/components/BulkBar.css +2 -0
  22. package/dist/packages/core/src/components/BulkBar.js +21 -2
  23. package/dist/packages/core/src/components/Button.d.ts +12 -2
  24. package/dist/packages/core/src/components/Button.js +70 -13
  25. package/dist/packages/core/src/components/Calendar.d.ts +6 -1
  26. package/dist/packages/core/src/components/Calendar.js +3 -3
  27. package/dist/packages/core/src/components/DatePicker.d.ts +7 -1
  28. package/dist/packages/core/src/components/DatePicker.js +25 -15
  29. package/dist/packages/core/src/components/Dropdown.d.ts +1 -0
  30. package/dist/packages/core/src/components/Dropdown.js +7 -5
  31. package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.d.ts +12 -1
  32. package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.js +25 -8
  33. package/dist/packages/core/src/components/EmptyState.d.ts +5 -1
  34. package/dist/packages/core/src/components/EmptyState.js +4 -1
  35. package/dist/packages/core/src/components/ErrorState.d.ts +5 -2
  36. package/dist/packages/core/src/components/ErrorState.js +3 -1
  37. package/dist/packages/core/src/components/FileUpload.d.ts +9 -1
  38. package/dist/packages/core/src/components/FileUpload.js +8 -2
  39. package/dist/packages/core/src/components/FilterSectionInfo.js +14 -2
  40. package/dist/packages/core/src/components/Header.js +8 -5
  41. package/dist/packages/core/src/components/HearingDeviceInfo.js +2 -1
  42. package/dist/packages/core/src/components/Icon.d.ts +2 -1
  43. package/dist/packages/core/src/components/Icon.js +4 -1
  44. package/dist/packages/core/src/components/Loader.css +28 -0
  45. package/dist/packages/core/src/components/Loader.d.ts +5 -1
  46. package/dist/packages/core/src/components/Loader.js +4 -2
  47. package/dist/packages/core/src/components/NoticeBox.d.ts +1 -1
  48. package/dist/packages/core/src/components/NoticeBox.js +15 -6
  49. package/dist/packages/core/src/components/PanelRow.js +2 -1
  50. package/dist/packages/core/src/components/PassCard/PassCard.d.ts +8 -2
  51. package/dist/packages/core/src/components/PassCard/PassCard.js +25 -10
  52. package/dist/packages/core/src/components/PassCard/passCardProduct.d.ts +1 -1
  53. package/dist/packages/core/src/components/PassCard/passCardProduct.js +11 -2
  54. package/dist/packages/core/src/components/Popup/ConfirmPopup.d.ts +11 -2
  55. package/dist/packages/core/src/components/Popup/ConfirmPopup.js +18 -3
  56. package/dist/packages/core/src/components/Popup/DeviceSideTable.js +10 -8
  57. package/dist/packages/core/src/components/Popup/ListPopup.js +6 -6
  58. package/dist/packages/core/src/components/Popup/LoadTablePopup.css +7 -0
  59. package/dist/packages/core/src/components/Popup/LoadTablePopup.js +27 -5
  60. package/dist/packages/core/src/components/Popup/Popup.d.ts +8 -0
  61. package/dist/packages/core/src/components/Popup/Popup.js +6 -3
  62. package/dist/packages/core/src/components/Popup/SaveTablePopup.d.ts +5 -1
  63. package/dist/packages/core/src/components/Popup/TrainingResultPopup.css +3 -0
  64. package/dist/packages/core/src/components/Popup/TrainingResultPopup.d.ts +5 -0
  65. package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +10 -4
  66. package/dist/packages/core/src/components/Popup/WordDataUpdatePopup.css +9 -0
  67. package/dist/packages/core/src/components/ProgressIndicator.css +5 -0
  68. package/dist/packages/core/src/components/ProgressIndicator.d.ts +3 -1
  69. package/dist/packages/core/src/components/ProgressIndicator.js +5 -3
  70. package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.d.ts +4 -1
  71. package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +27 -15
  72. package/dist/packages/core/src/components/Rating.d.ts +8 -2
  73. package/dist/packages/core/src/components/Rating.js +22 -8
  74. package/dist/packages/core/src/components/Report/ReportDocument.d.ts +10 -7
  75. package/dist/packages/core/src/components/Report/ReportDocument.js +39 -13
  76. package/dist/packages/core/src/components/RingChart.d.ts +1 -1
  77. package/dist/packages/core/src/components/RingChart.js +1 -1
  78. package/dist/packages/core/src/components/SegmentedControl.d.ts +4 -0
  79. package/dist/packages/core/src/components/SegmentedControl.js +9 -1
  80. package/dist/packages/core/src/components/ShortcutBadge.css +16 -1
  81. package/dist/packages/core/src/components/ShortcutBadge.d.ts +1 -0
  82. package/dist/packages/core/src/components/SideNav.js +8 -2
  83. package/dist/packages/core/src/components/SummaryStrip.d.ts +1 -1
  84. package/dist/packages/core/src/components/SummaryStrip.js +2 -2
  85. package/dist/packages/core/src/components/Table.d.ts +1 -1
  86. package/dist/packages/core/src/components/Table.js +4 -1
  87. package/dist/packages/core/src/components/Tabs.d.ts +1 -0
  88. package/dist/packages/core/src/components/Tabs.js +4 -1
  89. package/dist/packages/core/src/components/TextField.css +5 -6
  90. package/dist/packages/core/src/components/TextField.js +46 -3
  91. package/dist/packages/core/src/components/Toast.js +16 -2
  92. package/dist/packages/core/src/components/Tooltip/Tooltip.css +3 -1
  93. package/dist/packages/core/src/components/Tooltip/Tooltip.js +5 -2
  94. package/dist/packages/core/src/components/Tooltip/tooltipConstants.d.ts +10 -2
  95. package/dist/packages/core/src/components/Tooltip/tooltipConstants.js +10 -2
  96. package/dist/packages/core/src/components/TopBanner.d.ts +1 -1
  97. package/dist/packages/core/src/components/TopBanner.js +66 -8
  98. package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.d.ts +1 -0
  99. package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +65 -15
  100. package/dist/packages/core/src/components/Training/NoisePlayerContext.d.ts +3 -2
  101. package/dist/packages/core/src/components/Training/NoisePlayerContext.js +10 -4
  102. package/dist/packages/core/src/components/Training/TrainingBlockButton.css +7 -0
  103. package/dist/packages/core/src/components/Training/TrainingBlockButton.js +3 -1
  104. package/dist/packages/core/src/components/Training/TrainingPagePopup.d.ts +18 -5
  105. package/dist/packages/core/src/components/Training/TrainingPagePopup.js +93 -28
  106. package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.d.ts +1 -0
  107. package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +16 -3
  108. package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +29 -11
  109. package/dist/packages/core/src/components/Training/TrainingStepCard.js +19 -3
  110. package/dist/packages/core/src/components/Training/TrainingStepTitle.js +15 -1
  111. package/dist/packages/core/src/components/Training/TrainingWordCard.css +16 -0
  112. package/dist/packages/core/src/components/Training/TrainingWordCard.d.ts +6 -0
  113. package/dist/packages/core/src/components/Training/TrainingWordCard.js +7 -2
  114. package/dist/packages/core/src/components/Training/trainingBlockButtonTypes.d.ts +2 -2
  115. package/dist/packages/core/src/components/Training/trainingMissStats.d.ts +10 -0
  116. package/dist/packages/core/src/components/Training/trainingMissStats.js +19 -1
  117. package/dist/packages/core/src/components/Training/useFloatingFocus.d.ts +13 -0
  118. package/dist/packages/core/src/components/Training/useFloatingFocus.js +59 -0
  119. package/dist/packages/core/src/components/Training/useNoiseFade.js +20 -11
  120. package/dist/packages/core/src/components/Training/useProgressTicker.d.ts +0 -18
  121. package/dist/packages/core/src/components/Training/useProgressTicker.js +59 -14
  122. package/dist/packages/core/src/components/Training/useTTSPlayback.d.ts +1 -1
  123. package/dist/packages/core/src/components/Training/useTTSPlayback.js +56 -25
  124. package/dist/packages/core/src/components/Training/useWaveReady.d.ts +4 -1
  125. package/dist/packages/core/src/components/Training/useWaveReady.js +17 -8
  126. package/dist/packages/core/src/components/UpdateAlert.css +4 -2
  127. package/dist/packages/core/src/components/UpdateAlert.d.ts +3 -1
  128. package/dist/packages/core/src/components/UpdateAlert.js +21 -1
  129. package/dist/packages/core/src/components/Word/PhonemeSelectBlock.d.ts +2 -0
  130. package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +12 -4
  131. package/dist/packages/core/src/components/Word/WordSearchResultBlock.d.ts +6 -0
  132. package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +26 -13
  133. package/dist/packages/core/src/components/Word/WordTableBlock.js +24 -21
  134. package/dist/packages/core/src/components/Word/WordTableThumbnail.js +1 -1
  135. package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -10
  136. package/dist/packages/core/src/components/Word/useWordTablePrint.d.ts +1 -1
  137. package/dist/packages/core/src/components/Word/useWordTablePrint.js +9 -3
  138. package/dist/packages/core/src/components/Word/wordScoreStats.d.ts +14 -1
  139. package/dist/packages/core/src/components/Word/wordScoreStats.js +7 -3
  140. package/dist/packages/core/src/components/Word/wordTablePrint.js +15 -3
  141. package/dist/packages/core/src/components/Word/wordTableReview.d.ts +58 -1
  142. package/dist/packages/core/src/components/Word/wordTableReview.js +75 -17
  143. package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +9 -0
  144. package/dist/packages/core/src/components/Word/wordTableSettings.js +11 -0
  145. package/dist/packages/core/src/components/WordTable.css +10 -0
  146. package/dist/packages/core/src/components/WordTable.js +39 -26
  147. package/dist/packages/core/src/components/barChartTypes.d.ts +2 -2
  148. package/dist/packages/core/src/components/buttonTypes.d.ts +8 -0
  149. package/dist/packages/core/src/components/updateAlertStack.d.ts +2 -0
  150. package/dist/packages/core/src/components/updateAlertStack.js +68 -0
  151. package/dist/packages/core/src/components/useCalendarMonth.d.ts +4 -1
  152. package/dist/packages/core/src/components/useCalendarMonth.js +6 -3
  153. package/dist/packages/core/src/components/useComboboxSuggestions.d.ts +7 -0
  154. package/dist/packages/core/src/components/useComboboxSuggestions.js +21 -2
  155. package/dist/packages/core/src/components/wordTableLayout.d.ts +4 -0
  156. package/dist/packages/core/src/components/wordTableLayout.js +6 -0
  157. package/dist/packages/core/src/components.d.ts +2 -1
  158. package/dist/packages/core/src/components.js +1 -1
  159. package/dist/packages/core/src/hooks/bodyScrollLock.js +8 -2
  160. package/dist/packages/core/src/hooks/focusReturn.d.ts +12 -0
  161. package/dist/packages/core/src/hooks/focusReturn.js +49 -0
  162. package/dist/packages/core/src/hooks/touchToggle.d.ts +22 -0
  163. package/dist/packages/core/src/hooks/touchToggle.js +114 -0
  164. package/dist/packages/core/src/hooks/useAnchoredMenu.d.ts +12 -3
  165. package/dist/packages/core/src/hooks/useAnchoredMenu.js +37 -3
  166. package/dist/packages/core/src/hooks/useDailyTrainingSelection.d.ts +4 -2
  167. package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +23 -11
  168. package/dist/packages/core/src/hooks/useIcon.js +29 -5
  169. package/dist/packages/core/src/hooks/useLucideIcon.d.ts +4 -0
  170. package/dist/packages/core/src/hooks/useLucideIcon.js +4 -0
  171. package/dist/packages/core/src/hooks/usePendingPress.d.ts +9 -0
  172. package/dist/packages/core/src/hooks/usePendingPress.js +26 -0
  173. package/dist/packages/core/src/hooks/usePrefersReducedMotion.d.ts +15 -0
  174. package/dist/packages/core/src/hooks/usePrefersReducedMotion.js +26 -0
  175. package/dist/packages/core/src/hooks/useRelativeDay.d.ts +2 -2
  176. package/dist/packages/core/src/hooks/useRelativeDay.js +12 -5
  177. package/dist/packages/core/src/hooks/useTablePaging.d.ts +27 -2
  178. package/dist/packages/core/src/hooks/useTablePaging.js +37 -5
  179. package/dist/packages/core/src/hooks/useWordAttributeFilter.d.ts +7 -0
  180. package/dist/packages/core/src/hooks/useWordAttributeFilter.js +25 -0
  181. package/dist/packages/core/src/i18n/index.js +9 -1
  182. package/dist/packages/core/src/i18n/locales/en.json +24 -2
  183. package/dist/packages/core/src/i18n/locales/ko.json +24 -2
  184. package/dist/packages/core/src/tokens.css +5 -0
  185. package/dist/packages/core/src/types/wordSearch.d.ts +12 -0
  186. package/dist/packages/core/src/utils/csv.d.ts +7 -2
  187. package/dist/packages/core/src/utils/csv.js +12 -5
  188. package/dist/packages/core/src/utils/dailySelectionUtils.js +44 -14
  189. package/dist/packages/core/src/utils/date.d.ts +8 -0
  190. package/dist/packages/core/src/utils/date.js +44 -10
  191. package/dist/packages/core/src/utils/format.d.ts +1 -1
  192. package/dist/packages/core/src/utils/format.js +6 -7
  193. package/dist/packages/core/src/utils/noiseSettingsStore.d.ts +7 -3
  194. package/dist/packages/core/src/utils/phonemeConvert.d.ts +9 -0
  195. package/dist/packages/core/src/utils/phonemeConvert.js +11 -0
  196. package/dist/packages/core/src/utils/phonemePairSearch.d.ts +7 -1
  197. package/dist/packages/core/src/utils/phonemePairSearch.js +45 -21
  198. package/dist/packages/core/src/utils/safeHref.d.ts +8 -0
  199. package/dist/packages/core/src/utils/safeHref.js +24 -0
  200. package/dist/packages/core/src/utils/savedWordTables.d.ts +19 -3
  201. package/dist/packages/core/src/utils/savedWordTables.js +27 -19
  202. package/dist/packages/core/src/utils/textSort.d.ts +9 -0
  203. package/dist/packages/core/src/utils/textSort.js +37 -1
  204. package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +31 -0
  205. package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +163 -0
  206. package/dist/packages/core/src/utils/wordContrast.d.ts +4 -2
  207. package/dist/packages/core/src/utils/wordContrast.js +8 -2
  208. package/dist/packages/core/src/utils/wordTrainingGenerators.d.ts +5 -2
  209. package/dist/packages/core/src/utils/wordTrainingGenerators.js +52 -2
  210. package/dist/packages/core/src/utils/wordTrainingPool.js +2 -2
  211. package/dist/packages/core/src/utils/wordTrainingRuntime.js +15 -7
  212. package/dist/packages/core/src/utils/wordTrainingRuntimeTypes.d.ts +1 -1
  213. package/dist/packages/core/src/utils/wordTrainingRuntimeTypes.js +5 -4
  214. package/dist/packages/core/src/utils/wornDate.d.ts +12 -0
  215. package/dist/packages/core/src/utils/wornDate.js +15 -0
  216. package/dist/packages/core/src/version.d.ts +1 -1
  217. package/dist/packages/core/src/version.js +1 -1
  218. package/dist/utils.cjs.js +2 -2
  219. package/package.json +1 -1
@@ -16,7 +16,9 @@ export declare const hasSel: (v: string | string[] | null | undefined) => boolea
16
16
  * 두 (표준발음) 분해배열 A·B가 '음절 구조'에서 달라지는 자리가 있는지.
17
17
  * 초성 ㅇ(무음)은 '초성 없음', 종성 0은 '받침 없음'(둘 다 구조상 '비움')으로 본다.
18
18
  * 어떤 자리에서 초성 ㅇ 유무 또는 받침 유무가 뒤바뀌면, 자음 '교체'가 아니라 음절 '구조' 차이이므로
19
- * 청각 변별 오답으로 부적절 → true(제외 대상). 자음↔자음, 모음↔모음 교체는 false.
19
+ * 청각 변별 오답으로 부적절 → true(제외 대상). 자음↔자음 교체는 false.
20
+ * 모음은 **활음이 갈리면**(ㅏ↔ㅘ·ㅐ↔ㅙ·ㅑ↔ㅘ) 구조 차이, 활음이 같으면(ㅏ↔ㅓ·ㅑ↔ㅕ) 모음 교체라 false —
21
+ * 대립쌍 표('음소 구조 같은 단어만', phonemePairSearch)와 같은 함수(vowelGlidesDiffer)로 판정한다.
20
22
  */
21
23
  export declare function hasStructureContrast(A: Decomp, B: Decomp): boolean;
22
24
  /** 표준발음 기준 오답 필터에 필요한 의존성 */
@@ -27,7 +29,7 @@ export interface ContrastFilterOpts {
27
29
  pronOf: (w: string) => string;
28
30
  /** 표준발음 문자열 → 음절 분해(캐시 권장). sameStructureOnly일 때만 필요. */
29
31
  decompOf?: (pron: string) => Decomp;
30
- /** true면 구조 대립(초성 ㅇ 유무·받침 유무)도 제외(decompOf 필요). */
32
+ /** true면 구조 대립(초성 ㅇ 유무·활음 유무·종류·받침 유무)도 제외(decompOf 필요). */
31
33
  sameStructureOnly?: boolean;
32
34
  }
33
35
  /**
@@ -7,6 +7,7 @@
7
7
  * - buildMultiPartPosContrast: 초성/모음/종성 선택 시 '선택 위치 대립' 오답 사전계산.
8
8
  */
9
9
  import { CHOSEONG, JUNGSEONG, JONGSEONG } from './koreanSyllable';
10
+ import { vowelGlidesDiffer } from './phonemeConvert';
10
11
  /** 파트 인덱스 → 자모 배열 (이 파일 내부 전용) */
11
12
  const PART_ARR = { cho: CHOSEONG, jung: JUNGSEONG, jong: JONGSEONG };
12
13
  /** 선택 값(단일/배열)이 실제로 지정됐는지 */
@@ -15,7 +16,9 @@ export const hasSel = (v) => (Array.isArray(v) ? v.length > 0 : !!v);
15
16
  * 두 (표준발음) 분해배열 A·B가 '음절 구조'에서 달라지는 자리가 있는지.
16
17
  * 초성 ㅇ(무음)은 '초성 없음', 종성 0은 '받침 없음'(둘 다 구조상 '비움')으로 본다.
17
18
  * 어떤 자리에서 초성 ㅇ 유무 또는 받침 유무가 뒤바뀌면, 자음 '교체'가 아니라 음절 '구조' 차이이므로
18
- * 청각 변별 오답으로 부적절 → true(제외 대상). 자음↔자음, 모음↔모음 교체는 false.
19
+ * 청각 변별 오답으로 부적절 → true(제외 대상). 자음↔자음 교체는 false.
20
+ * 모음은 **활음이 갈리면**(ㅏ↔ㅘ·ㅐ↔ㅙ·ㅑ↔ㅘ) 구조 차이, 활음이 같으면(ㅏ↔ㅓ·ㅑ↔ㅕ) 모음 교체라 false —
21
+ * 대립쌍 표('음소 구조 같은 단어만', phonemePairSearch)와 같은 함수(vowelGlidesDiffer)로 판정한다.
19
22
  */
20
23
  export function hasStructureContrast(A, B) {
21
24
  if (A.length !== B.length)
@@ -26,6 +29,9 @@ export function hasStructureContrast(A, B) {
26
29
  continue;
27
30
  if (sa.cho !== sb.cho && (CHOSEONG[sa.cho] === 'ㅇ') !== (CHOSEONG[sb.cho] === 'ㅇ'))
28
31
  return true;
32
+ /* 전에는 초성 ㅇ·받침만 봐, 대립쌍 표에서는 '구조가 달라' 빠지는 사↔솨 가 같은 '음소 구조 같은 단어만' 훈련에서 오답으로 나왔다 */
33
+ if (sa.jung !== sb.jung && vowelGlidesDiffer(JUNGSEONG[sa.jung], JUNGSEONG[sb.jung]))
34
+ return true;
29
35
  if (sa.jong !== sb.jong && (sa.jong === 0) !== (sb.jong === 0))
30
36
  return true;
31
37
  }
@@ -188,7 +194,7 @@ position) {
188
194
  }
189
195
  if (!good)
190
196
  continue;
191
- // 구조(초성 ㅇ 유무·받침 유무) 대립 제외 — 발음 분해 기준
197
+ // 구조(초성 ㅇ 유무·활음·받침 유무) 대립 제외 — 발음 분해 기준
192
198
  if (!(sameStructureOnly && hasStructureContrast(A.dPron, D.dPron)))
193
199
  dists.push(D.w);
194
200
  }
@@ -24,7 +24,9 @@ export declare function mergeQuestionText(parts: string[], phoneme?: boolean): s
24
24
  */
25
25
  export declare function getMergeCardTitle(typeKey: string | undefined, parts: string[]): string;
26
26
  /**
27
- * @public 바꾸기(대치) 카드 질문 문장 — 러너·프리워밍 공용. item의 word/changedFrom/changedTo로 구성.
27
+ * @public 바꾸기(대치) 카드 질문 문장 — 러너·프리워밍 공용.
28
+ * 음소 바꾸기(1·2음절)는 바뀌는 음소를 말한다(`"옆"에서 모음 "ㅕ"를 "ㅞ"로 바꾸면…`, `"사과"의 "과"에서 초성 "ㄱ"을 "ㅎ"으로…`,
29
+ * 받침이 생기면 `"가"에 받침 "ㄴ"을 더하면…`). 음절 바꾸기는 음절을 말한다. item의 word/changedFrom/changedTo로 구성.
28
30
  * 프리워밍이 만든 문장이 러너 재생 문장과 문자 단위로 같아야 캐시 키가 일치하므로 반드시 이 함수를 공유한다.
29
31
  */
30
32
  export declare function getChangeCardTitle(typeKey: string | undefined, item: Record<string, unknown>): string;
@@ -92,7 +94,8 @@ export declare function buildDropContrastProblem(word: string, comp: Composition
92
94
  export declare function buildDropContrast3Problem(word: string, comp: CompositionFilter | null, pronMap: Map<string, string>, phonemeView: 'char' | 'pron', wordSet: Set<string>, dropPos?: number | null): TrainingProblem | null;
93
95
  /**
94
96
  * @public 합성어 부분 탈락 → [문제단어(합성어), 남는 실단어(정답), 그 단어의 대립쌍 실단어 오답 2].
95
- * 합성어 근사: 앞/뒤로 나눈 두 조각이 '둘 다 실단어'(wordSet)인 첫 조합을 채택(예: 손수건=손+수건).
97
+ * 합성어 근사: 앞/뒤로 나눈 두 조각이 '둘 다 실단어'(wordSet)이고 **적어도 한 조각이 2음절 이상**인 조합을 채택(예: 손수건=손+수건).
98
+ * 1음절 조각 둘(하품=하+품)은 합성어로 보지 않는다 — 그래서 2음절 낱말은 늘 null.
96
99
  * 정답·오답 전부 실단어라 발음이 그대로. comp(훈련음소) 지정 시 남는 단어의 그 자리를 대립, 없으면 전 음절.
97
100
  * dropPos: 0=어두 어근(앞) 탈락, 1=어말 어근(뒤) 탈락. 결과의 `dropped` 에 뺀 조각을 담는다. 편집기·런타임 공용.
98
101
  */
@@ -98,14 +98,59 @@ export function mergeQuestionText(parts, phoneme = false) {
98
98
  export function getMergeCardTitle(typeKey, parts) {
99
99
  return mergeQuestionText(parts, typeKey === 'MERGE_2_PHONEMES' || typeKey === 'MERGE_3_PHONEMES');
100
100
  }
101
+ /** 조사를 붙일 때 읽는 꼴 — 자모면 낱자 이름(ㅂ→비읍), 아니면 그대로 */
102
+ const josaBaseOf = (x) => JAMO_NAME[x] ?? x;
103
+ const PART_LABEL = { cho: '초성', jung: '모음', jong: '받침' };
101
104
  /**
102
- * @public 바꾸기(대치) 카드 질문 문장 — 러너·프리워밍 공용. item의 word/changedFrom/changedTo로 구성.
105
+ * 두 음절 사이에서 바뀐 음소 하나. 철자로 한 자리만 다르면 그 자리, 아니면 끝소리 규칙 발음([옆→엽]↔[웹])으로 한 자리만
106
+ * 다르면 그 자리. 받침이 생기거나 없어지면 add·remove. 둘 이상이 다르면 null(음소 하나로 말할 수 없다).
107
+ */
108
+ function changedPhonemeOf(a, b) {
109
+ const diffOf = (x, y) => {
110
+ const sx = [...x].length === 1 ? parseSyllable(x) : null;
111
+ const sy = [...y].length === 1 ? parseSyllable(y) : null;
112
+ if (!sx || !sy)
113
+ return null;
114
+ const parts = ['cho', 'jung', 'jong'].filter(p => sx[p] !== sy[p]);
115
+ return parts.length === 1 ? { part: parts[0], sx, sy } : null;
116
+ };
117
+ const d = diffOf(a, b) ?? diffOf(syllablePron(a), syllablePron(b));
118
+ if (!d)
119
+ return null;
120
+ const arr = d.part === 'cho' ? CHOSEONG : d.part === 'jung' ? JUNGSEONG : JONGSEONG;
121
+ const from = arr[d.sx[d.part]], to = arr[d.sy[d.part]];
122
+ const kind = d.part === 'jong' && !from ? 'add' : d.part === 'jong' && !to ? 'remove' : 'swap';
123
+ return { part: d.part, from, to, kind };
124
+ }
125
+ /**
126
+ * @public 바꾸기(대치) 카드 질문 문장 — 러너·프리워밍 공용.
127
+ * 음소 바꾸기(1·2음절)는 바뀌는 음소를 말한다(`"옆"에서 모음 "ㅕ"를 "ㅞ"로 바꾸면…`, `"사과"의 "과"에서 초성 "ㄱ"을 "ㅎ"으로…`,
128
+ * 받침이 생기면 `"가"에 받침 "ㄴ"을 더하면…`). 음절 바꾸기는 음절을 말한다. item의 word/changedFrom/changedTo로 구성.
103
129
  * 프리워밍이 만든 문장이 러너 재생 문장과 문자 단위로 같아야 캐시 키가 일치하므로 반드시 이 함수를 공유한다.
104
130
  */
105
131
  export function getChangeCardTitle(typeKey, item) {
106
132
  const word = String(item?.word ?? item?.original ?? '');
107
133
  const changedFrom = String(item?.changedFrom ?? '');
108
134
  const changedTo = String(item?.changedTo ?? '');
135
+ /* 음소 바꾸기는 **바뀌는 음소**를 말한다 — 전에는 바뀐 음절을 말해 1음절은 "'옆'을 '웹'으로 바꾸면" 이 곧 정답이었고,
136
+ 2음절도 새 음절을 불러 줘 음소를 다룰 일이 없었다. 음소 하나로 설명이 안 되는 자료(옛 저장 문제 등)는 전 문장으로 물러선다. */
137
+ if (typeKey === 'CHANGE_1_PHONEME_IN_1SYLL' || typeKey === 'CHANGE_1_PHONEME_IN_2SYLL') {
138
+ const one = typeKey === 'CHANGE_1_PHONEME_IN_1SYLL';
139
+ const from = one ? (changedFrom || word) : changedFrom;
140
+ const change = changedPhonemeOf(from, changedTo);
141
+ if (change) {
142
+ // 1음절: "옆"에서 … / 2음절: "사과"의 "과"에서 … ('더하면'은 에서 대신 에)
143
+ const head = one ? `"${word}"` : `"${word}"의 "${from}"`;
144
+ const 곳 = change.kind === 'add' ? '에' : '에서';
145
+ const f = `"${change.from}"${josaEulReul(josaBaseOf(change.from))}`;
146
+ const t = `"${change.to}"${josaEuRo(josaBaseOf(change.to))}`;
147
+ if (change.kind === 'add')
148
+ return `${head}${곳} 받침 "${change.to}"${josaEulReul(josaBaseOf(change.to))} 더하면 무엇이 되나요?`;
149
+ if (change.kind === 'remove')
150
+ return `${head}${곳} 받침 ${f} 빼면 무엇이 되나요?`;
151
+ return `${head}${곳} ${PART_LABEL[change.part]} ${f} ${t} 바꾸면 무엇이 되나요?`;
152
+ }
153
+ }
109
154
  if (typeKey === 'CHANGE_1_PHONEME_IN_1SYLL') {
110
155
  // 1음절 단어 전체가 바뀌는 케이스 — "에서 X를" 없이 단순 표현
111
156
  return `"${word}"${josaEulReul(word)} "${changedTo}"${josaEuRo(changedTo)} 바꾸면 무엇이 되나요?`;
@@ -365,7 +410,8 @@ export function buildDropContrast3Problem(word, comp, pronMap, phonemeView, word
365
410
  }
366
411
  /**
367
412
  * @public 합성어 부분 탈락 → [문제단어(합성어), 남는 실단어(정답), 그 단어의 대립쌍 실단어 오답 2].
368
- * 합성어 근사: 앞/뒤로 나눈 두 조각이 '둘 다 실단어'(wordSet)인 첫 조합을 채택(예: 손수건=손+수건).
413
+ * 합성어 근사: 앞/뒤로 나눈 두 조각이 '둘 다 실단어'(wordSet)이고 **적어도 한 조각이 2음절 이상**인 조합을 채택(예: 손수건=손+수건).
414
+ * 1음절 조각 둘(하품=하+품)은 합성어로 보지 않는다 — 그래서 2음절 낱말은 늘 null.
369
415
  * 정답·오답 전부 실단어라 발음이 그대로. comp(훈련음소) 지정 시 남는 단어의 그 자리를 대립, 없으면 전 음절.
370
416
  * dropPos: 0=어두 어근(앞) 탈락, 1=어말 어근(뒤) 탈락. 결과의 `dropped` 에 뺀 조각을 담는다. 편집기·런타임 공용.
371
417
  */
@@ -383,6 +429,10 @@ export function buildDropCompoundProblem(word, comp, pronMap, phonemeView, wordS
383
429
  for (const { remain, dropped, droppedEnd } of shuffled(candidates)) {
384
430
  if (dropPos != null && droppedEnd !== dropPos)
385
431
  continue; // 위치(삭제할 어근) 지정 시 그 끝만.
432
+ /* 1음절 조각 둘은 합성어로 보지 않는다 — 두 글자가 우연히 모두 낱말이면(하품 = 하 + 품, 바지 = 바 + 지) 합성어로 읽어
433
+ "하품에서 품이 없어지면" 을 냈다. 2음절 이상 조각이 하나는 있어야 한다(손수건 = 손 + 수건). 자료에 합성어 표시가 없어 근사다. */
434
+ if ([...remain].length === 1 && [...dropped].length === 1)
435
+ continue;
386
436
  if (remain === word || !wordSet.has(remain) || !wordSet.has(dropped))
387
437
  continue;
388
438
  const rChars = [...remain];
@@ -251,7 +251,7 @@ async function _buildUsablePool(opts) {
251
251
  const { difficulty = null, choseong = null, composition = null, targetSyllable = null, excludeNoImage = false } = opts;
252
252
  // 난이도 등급('1'~'4') 필터 — 정답(후보) 단어에만 적용(구성 필터와 동일 원칙, 오답 풀은 무관)
253
253
  const gradeSet = opts.grade && opts.grade.length > 0 ? new Set(opts.grade) : null;
254
- // 음소 구조 같은 단어만(초성 ㅇ 유무·받침 유무 대립 제외) — 기본 true
254
+ // 음소 구조 같은 단어만(초성 ㅇ 유무·활음·받침 유무 대립 제외) — 기본 true
255
255
  const sameStructureOnly = opts.sameStructureOnly ?? true;
256
256
  // 음소 선택 방식 — 정답 검색 기준(글자입력=철자 / 발음입력=발음). 대립 비교는 두 모드 모두 발음.
257
257
  const phonemeView = opts.phonemeView === 'pron' ? 'pron' : 'char';
@@ -475,7 +475,7 @@ async function _buildUsablePool(opts) {
475
475
  usable = fallbackSample.filter(other => other !== w.w && classifyPairDifficulty(w.w, other) === difficulty && pronOfWord(other) !== wPron);
476
476
  }
477
477
  }
478
- // 음절 구조 대립(초성 ㅇ 유무·받침 유무) 제외 — 자음/모음 '교체'가 아닌 구조 차이는 청각 변별 오답으로 부적절.
478
+ // 음절 구조 대립(초성 ㅇ 유무·활음·받침 유무) 제외 — 자음/모음 '교체'가 아닌 구조 차이는 청각 변별 오답으로 부적절.
479
479
  // '음소 구조 같은 단어만'(sameStructureOnly)일 때만. posContrast 는 buildMultiPartPosContrast 안에서 적용. random/null 제외.
480
480
  if (sameStructureOnly && usable.length > 0 && (isTyped || (isCount && !posContrast))) {
481
481
  const wPronDecomp = decompOf(pronOfWord(w.w));
@@ -15,7 +15,7 @@ import { getPairIndex } from './wordPairIndex';
15
15
  import { wordContainsPhoneme, syllableLength, parseSyllable, CHOSEONG, JUNGSEONG, JONGSEONG, JONG_DECOMPOSE } from './koreanSyllable';
16
16
  export { CAPABILITY, cardGroupOf, isValidQuestionWord, hasQuestionWordConstraint, isManualOnlyType, QUESTION_WORD_RULE, MANUAL_ONLY_TYPES } from './wordTrainingRuntimeTypes';
17
17
  import { CAPABILITY, QUESTION_WORD_RULE } from './wordTrainingRuntimeTypes';
18
- import { buildDropContrastProblem, buildDropContrast3Problem, buildDropCompoundProblem, buildDropFinalConsonantProblem, buildRemoveConsonantProblem, buildChangeWholeSyllableProblem, buildSyllableMaskIndex } from './wordTrainingGenerators';
18
+ import { buildDropContrastProblem, buildDropContrast3Problem, buildDropCompoundProblem, buildDropFinalConsonantProblem, buildRemoveConsonantProblem, buildChangeWholeSyllableProblem, buildSyllableMaskIndex, primarySelPart, syllMatchesComp } from './wordTrainingGenerators';
19
19
  /* 재료 고르기(풀 추리기·캐시·오답 샘플링)는 wordTrainingPool 로 갈라 두었다. */
20
20
  import { MVP, buildUsablePool, filterByCachedImage, getPronMap, nextId, pickOne, sample, sampleDistinctPron, _poolCacheKey, _EMPTY_SET, answerWordFilter, pronOfWord, } from './wordTrainingPool';
21
21
  // ────────────────────────────────────────────────────────────────────────────
@@ -60,6 +60,7 @@ async function genIncluded(opts) {
60
60
  const allPool = (await buildUsablePool({ ...opts, difficulty: null }));
61
61
  if (allPool.length < choicesPerItem)
62
62
  return [];
63
+ const comp = opts.composition ?? null;
63
64
  const results = [];
64
65
  const candidates = shuffle([...allPool]);
65
66
  for (const entry of candidates) {
@@ -70,7 +71,12 @@ async function genIncluded(opts) {
70
71
  // '맏이(마지)'에서 '맏'이 나와 실제로는 들리지 않는 소리를 문제로 내게 된다. 표준발음에서 뽑는다.
71
72
  const pron = entry.word.pron || word;
72
73
  const syllables = [...pron]; // 한 글자 = 한 음절
73
- const targetSyllable = pickOne(syllables);
74
+ /* 음소를 골랐으면 **그 음소를 가진 음절**을 들려준다 — 전에는 아무 음절이나 골라 초성 ㄱ 을 골라도 '가방' 의 '방' 을
75
+ 들려주었다(고른 음소와 무관한 훈련). 들리는 음절(발음)에 그 음소가 없으면 전처럼 아무 음절. */
76
+ const 고른음절 = comp && primarySelPart(comp)
77
+ ? syllables.filter(s => { const p = parseSyllable(s); return !!p && syllMatchesComp(p, comp); })
78
+ : [];
79
+ const targetSyllable = pickOne(고른음절.length ? 고른음절 : syllables);
74
80
  if (!targetSyllable)
75
81
  continue;
76
82
  // 오답: targetSyllable이 발음에 포함되지 않고, 정답과 음절수(글자수)가 같으며, 발음이 다른 단어.
@@ -426,8 +432,9 @@ async function genDrop1From3Syll(opts) {
426
432
  /** DROP_PART_FROM_COMPOUND — 합성어(앞/뒤 조각이 둘 다 실단어)에서 한 어근 탈락. 편집기와 동일 코어 빌더 사용. */
427
433
  async function genDropCompound(opts) {
428
434
  const { count = 10 } = opts;
429
- // 합성어 후보 — 2음절 이상. 훈련음소는 선택적(있으면 남는 단어의 그 자리를 대립으로 좁힘).
430
- const pool = (await buildUsablePool({ ...opts })).filter(e => e.word.s >= 2);
435
+ // 합성어 후보 — 3음절 이상. 훈련음소는 선택적(있으면 남는 단어의 그 자리를 대립으로 좁힘).
436
+ // 2음절 낱말은 1음절 조각 둘로만 나뉘어 합성어가 될 수 없다(buildDropCompoundProblem) — 3음절부터
437
+ const pool = (await buildUsablePool({ ...opts })).filter(e => e.word.s >= 3);
431
438
  const pronMap = getPronMap();
432
439
  const wordSet = 탈락낱말집합(opts);
433
440
  const phonemeView = opts.phonemeView ?? 'char';
@@ -497,15 +504,16 @@ function realWordsOfLen(N, opts, pred) {
497
504
  }
498
505
  return out;
499
506
  }
500
- /** MERGE_2_PHONEMES — 초성+모음 합쳐 1음절(받침 없음). 실단어 데이터 기반 */
507
+ /** MERGE_2_PHONEMES — 초성+모음 합쳐 1음절(받침 없음·초성이 소리 있는 자음). 실단어 데이터 기반 */
501
508
  function genMerge2Phonemes(opts) {
502
- const pool = realWordsOfLen(1, opts, s => s.jong === 0);
509
+ // 편집기 검증과 같은 규칙 한 벌(QUESTION_WORD_RULE) — 초성 ㅇ(소리 없음)은 합칠 음소가 아니라 뺀다
510
+ const pool = realWordsOfLen(1, opts, QUESTION_WORD_RULE.MERGE_2_PHONEMES.syllable);
503
511
  return sample(pool, opts.count ?? 10).map((e, i) => {
504
512
  const s = parseSyllable(e.w);
505
513
  return { id: nextId('M2P-item'), index: i + 1, parts: [CHOSEONG[s.cho], JUNGSEONG[s.jung]], answer: e.w, word: e.w };
506
514
  });
507
515
  }
508
- /** MERGE_3_PHONEMES — 초성+모음+종성 합쳐 1음절(홑받침). 실단어 데이터 기반 */
516
+ /** MERGE_3_PHONEMES — 초성+모음+종성 합쳐 1음절(홑받침·초성이 소리 있는 자음). 실단어 데이터 기반 */
509
517
  function genMerge3Phonemes(opts) {
510
518
  // 홑받침만(겹받침은 음소 2개라 '3음소'가 아니게 됨) — 편집기 검증과 같은 규칙 한 벌(QUESTION_WORD_RULE)
511
519
  const pool = realWordsOfLen(1, opts, QUESTION_WORD_RULE.MERGE_3_PHONEMES.syllable);
@@ -35,7 +35,7 @@ export interface GenOptions {
35
35
  */
36
36
  excludeNoImage?: boolean;
37
37
  /**
38
- * true(기본) 이면 오답이 정답과 '음절 구조'(초성 ㅇ 유무·받침 유무)가 같은 단어만 되도록 강제.
38
+ * true(기본) 이면 오답이 정답과 '음절 구조'(초성 ㅇ 유무·활음(ㅏ↔ㅘ)·받침 유무)가 같은 단어만 되도록 강제(대립쌍 표와 같은 정의).
39
39
  * false 이면 구조가 다른 대립(예: 발↔알, 파↔판)도 오답으로 허용.
40
40
  */
41
41
  sameStructureOnly?: boolean;
@@ -47,11 +47,12 @@ export const QUESTION_WORD_RULE = {
47
47
  MERGE_2_SYLLABLES: { syllables: 2, syllable: () => true },
48
48
  // 음절 3개 합치기: 정답 단어가 항상 3음절이어야 함(genMerge3Syllables와 동일 제약)
49
49
  MERGE_3_SYLLABLES: { syllables: 3, syllable: () => true },
50
- // 음소 2개 합치기(초성+중성): 정답은 받침 없는 1음절(CV) — genMerge2Phonemes(s.jong===0)와 동일 제약
51
- MERGE_2_PHONEMES: { syllables: 1, syllable: s => s.jong === 0 },
50
+ // 음소 2개 합치기(초성+중성): 정답은 받침 없는 1음절(CV) — genMerge2Phonemes 가 이 규칙을 쓴다.
51
+ // 초성 ㅇ 은 소리가 없어 합칠 음소가 아니다 — 전에는 '아' 를 "ㅇ과 ㅏ를 합치면" 으로 냈다(실제로는 음소 하나). 뺀다.
52
+ MERGE_2_PHONEMES: { syllables: 1, syllable: s => s.jong === 0 && s.cho !== _IEUNG_CHO },
52
53
  // 음소 3개 합치기(초성+중성+종성): 정답은 **홑받침** 1음절(CVC) — 겹받침(닭·값)은 음소 넷이라 뺀다(genMerge3Phonemes 가 이 규칙을 쓴다).
53
- // 전에는 편집기가 '닭' 을 받아 "ㄷ, ㅏ, ㄺ 을 합치면" 을 냈다.
54
- MERGE_3_PHONEMES: { syllables: 1, syllable: s => s.jong > 0 && !JONG_DECOMPOSE[s.jong] },
54
+ // 전에는 편집기가 '닭' 을 받아 "ㄷ, ㅏ, ㄺ 을 합치면" 을 냈다. 초성 ㅇ('앙' = 음소 둘)도 2개 합치기와 같은 까닭으로 뺀다.
55
+ MERGE_3_PHONEMES: { syllables: 1, syllable: s => s.jong > 0 && !JONG_DECOMPOSE[s.jong] && s.cho !== _IEUNG_CHO },
55
56
  };
56
57
  /** @public 자동 문제생성이 불가하고 수동 구성만 지원하는 타입. 현재는 없음.
57
58
  * (합성어 부분 탈락은 앞/뒤 조각이 모두 실단어인 합성어를 휴리스틱으로 자동 추천 — buildDropCompoundProblem.) */
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 기기 착용일(연·월·일 세 칸) 을 글자로 — 표(DeviceSideTable)·패널(PanelRowDevice)·HearingDeviceInfo 가 함께 쓴다.
3
+ *
4
+ * 타입은 문자열이지만 서버에서는 **월·일이 null 로 오기도 한다**(연도만 적은 옛 기록). 그대로 끼우면 화면에
5
+ * `2026.null.null` 이 찍혔고, 정렬 열쇠를 만들 때는 `null.padStart` 에서 표가 통째로 터졌다. 빈 값은 '' 로 다룬다.
6
+ */
7
+ type 칸값 = string | number | null | undefined;
8
+ /** 화면에 적을 착용일 — `2026.05.02`. 뒤쪽 빈 칸은 떼고(`2026`), 전부 비면 ''. */
9
+ export declare function formatWornDate(year: 칸값, month: 칸값, day: 칸값): string;
10
+ /** 착용일 정렬 열쇠 — 자리수를 채워(`2026-05-02`) 글자 비교가 날짜 차례와 같게. 연도가 없으면 ''. */
11
+ export declare function wornDateSortKey(year: 칸값, month: 칸값, day: 칸값): string;
12
+ export {};
@@ -0,0 +1,15 @@
1
+ const 글자 = (v) => (v == null ? '' : String(v).trim());
2
+ /** 화면에 적을 착용일 — `2026.05.02`. 뒤쪽 빈 칸은 떼고(`2026`), 전부 비면 ''. */
3
+ export function formatWornDate(year, month, day) {
4
+ const 조각 = [글자(year), 글자(month), 글자(day)];
5
+ while (조각.length > 0 && 조각[조각.length - 1] === '')
6
+ 조각.pop();
7
+ return 조각.join('.');
8
+ }
9
+ /** 착용일 정렬 열쇠 — 자리수를 채워(`2026-05-02`) 글자 비교가 날짜 차례와 같게. 연도가 없으면 ''. */
10
+ export function wornDateSortKey(year, month, day) {
11
+ const y = 글자(year);
12
+ if (!y)
13
+ return '';
14
+ return `${y}-${글자(month).padStart(2, '0')}-${글자(day).padStart(2, '0')}`;
15
+ }
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.149.0";
49
+ export declare const CORE_VERSION = "0.151.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.149.0';
49
+ export const CORE_VERSION = '0.151.0';