@ihabdevteam/core 0.322.0 → 0.324.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 (198) hide show
  1. package/CHANGELOG.md +557 -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 +55 -17
  8. package/dist/packages/core/src/components/Accordion.js +3 -3
  9. package/dist/packages/core/src/components/Alert.js +2 -2
  10. package/dist/packages/core/src/components/BarChart.js +120 -119
  11. package/dist/packages/core/src/components/BottomNav.js +18 -18
  12. package/dist/packages/core/src/components/BulkBar.js +25 -25
  13. package/dist/packages/core/src/components/Button.js +16 -16
  14. package/dist/packages/core/src/components/Calendar.js +17 -17
  15. package/dist/packages/core/src/components/ChartSection.js +30 -30
  16. package/dist/packages/core/src/components/CountTile.js +38 -38
  17. package/dist/packages/core/src/components/DatePicker.js +59 -59
  18. package/dist/packages/core/src/components/Dialog.js +14 -14
  19. package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.js +8 -8
  20. package/dist/packages/core/src/components/FilterPanel.js +6 -6
  21. package/dist/packages/core/src/components/FilterSectionInfo.js +6 -6
  22. package/dist/packages/core/src/components/GameIntro.js +1 -1
  23. package/dist/packages/core/src/components/Icon.js +2 -2
  24. package/dist/packages/core/src/components/ItemActions.js +16 -16
  25. package/dist/packages/core/src/components/LandingHeader.js +6 -6
  26. package/dist/packages/core/src/components/LineChart.js +24 -24
  27. package/dist/packages/core/src/components/Loader.js +5 -5
  28. package/dist/packages/core/src/components/NoticeBox.js +3 -3
  29. package/dist/packages/core/src/components/PageTitle.js +2 -2
  30. package/dist/packages/core/src/components/Pagination.js +4 -4
  31. package/dist/packages/core/src/components/Panel.js +2 -2
  32. package/dist/packages/core/src/components/PanelRow.js +6 -13
  33. package/dist/packages/core/src/components/PassCard/PassCard.js +9 -9
  34. package/dist/packages/core/src/components/PassCard/passCardProduct.js +2 -2
  35. package/dist/packages/core/src/components/PaymentPlan.js +5 -5
  36. package/dist/packages/core/src/components/Popup/ConfirmPopup.js +29 -29
  37. package/dist/packages/core/src/components/Popup/CouponInputPopup.js +2 -2
  38. package/dist/packages/core/src/components/Popup/DeviceSideTable.js +18 -25
  39. package/dist/packages/core/src/components/Popup/ListPopup.js +3 -3
  40. package/dist/packages/core/src/components/Popup/LoadTablePopup.js +7 -7
  41. package/dist/packages/core/src/components/Popup/Popup.js +27 -27
  42. package/dist/packages/core/src/components/Popup/SaveTablePopup.js +5 -5
  43. package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +4 -4
  44. package/dist/packages/core/src/components/Popup/customerDetailUtils.js +18 -18
  45. package/dist/packages/core/src/components/Popup/useDiscardGuard.js +4 -4
  46. package/dist/packages/core/src/components/ProgressIndicator.js +5 -5
  47. package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +16 -16
  48. package/dist/packages/core/src/components/Rating.js +20 -20
  49. package/dist/packages/core/src/components/Report/ReportDocument.js +21 -21
  50. package/dist/packages/core/src/components/RingChart.js +5 -5
  51. package/dist/packages/core/src/components/SectionHeadFloat.js +15 -15
  52. package/dist/packages/core/src/components/SegmentedControl.js +34 -34
  53. package/dist/packages/core/src/components/SideNav.js +37 -37
  54. package/dist/packages/core/src/components/Slider.js +2 -2
  55. package/dist/packages/core/src/components/SoundCompass.js +5 -5
  56. package/dist/packages/core/src/components/Table.js +257 -98
  57. package/dist/packages/core/src/components/Tabs.js +12 -12
  58. package/dist/packages/core/src/components/TextField.js +34 -34
  59. package/dist/packages/core/src/components/Toast.js +6 -6
  60. package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +4 -4
  61. package/dist/packages/core/src/components/Tooltip/Tooltip.js +22 -22
  62. package/dist/packages/core/src/components/Tooltip/TrainingTooltip.js +4 -4
  63. package/dist/packages/core/src/components/TopBanner.js +28 -28
  64. package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +11 -11
  65. package/dist/packages/core/src/components/Training/CategoryIcon.js +2 -2
  66. package/dist/packages/core/src/components/Training/NoisePlayerContext.js +12 -12
  67. package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +2 -2
  68. package/dist/packages/core/src/components/Training/TrainingBlock.js +11 -11
  69. package/dist/packages/core/src/components/Training/TrainingBlockButton.js +10 -10
  70. package/dist/packages/core/src/components/Training/TrainingBlockGrid.js +21 -21
  71. package/dist/packages/core/src/components/Training/TrainingCard.css +13 -1
  72. package/dist/packages/core/src/components/Training/TrainingCard.js +2 -2
  73. package/dist/packages/core/src/components/Training/TrainingCardGroup.js +6 -6
  74. package/dist/packages/core/src/components/Training/TrainingPagePopup.js +81 -81
  75. package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +17 -17
  76. package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +127 -127
  77. package/dist/packages/core/src/components/Training/TrainingStepCard.js +18 -18
  78. package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +4 -4
  79. package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +12 -12
  80. package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +12 -12
  81. package/dist/packages/core/src/components/Training/TrainingStepTitle.js +4 -4
  82. package/dist/packages/core/src/components/Training/TrainingWordCard.js +17 -17
  83. package/dist/packages/core/src/components/Training/WordTablePopup.js +5 -5
  84. package/dist/packages/core/src/components/Training/audioPlaybackRules.js +8 -8
  85. package/dist/packages/core/src/components/Training/audioTimeline.js +25 -25
  86. package/dist/packages/core/src/components/Training/oneShotSound.js +13 -13
  87. package/dist/packages/core/src/components/Training/trainingMissStats.js +20 -20
  88. package/dist/packages/core/src/components/Training/trainingPopupUtils.js +7 -7
  89. package/dist/packages/core/src/components/Training/useAudioPreview.js +15 -15
  90. package/dist/packages/core/src/components/Training/useFloatingFocus.js +14 -14
  91. package/dist/packages/core/src/components/Training/useNoiseFade.js +22 -22
  92. package/dist/packages/core/src/components/Training/useObjectUrls.js +2 -2
  93. package/dist/packages/core/src/components/Training/usePlaybackSequencer.d.ts +2 -2
  94. package/dist/packages/core/src/components/Training/usePlaybackSequencer.js +23 -23
  95. package/dist/packages/core/src/components/Training/useProgressTicker.js +33 -33
  96. package/dist/packages/core/src/components/Training/useTTSPlayback.js +17 -17
  97. package/dist/packages/core/src/components/Training/useWaveReady.js +11 -11
  98. package/dist/packages/core/src/components/UpdateAlert.js +6 -6
  99. package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +7 -1
  100. package/dist/packages/core/src/components/Word/TableFilterPanel.js +3 -3
  101. package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +10 -10
  102. package/dist/packages/core/src/components/Word/WordTableBlock.js +28 -28
  103. package/dist/packages/core/src/components/Word/WordTableButton.css +16 -2
  104. package/dist/packages/core/src/components/Word/WordTableThumbnail.css +6 -1
  105. package/dist/packages/core/src/components/Word/generatedDraft.js +14 -14
  106. package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -7
  107. package/dist/packages/core/src/components/Word/wordScoreStats.js +15 -15
  108. package/dist/packages/core/src/components/Word/wordTablePrint.js +10 -10
  109. package/dist/packages/core/src/components/Word/wordTableRecommend.js +11 -11
  110. package/dist/packages/core/src/components/Word/wordTableReview.js +28 -28
  111. package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +8 -11
  112. package/dist/packages/core/src/components/Word/wordTableSettings.js +48 -36
  113. package/dist/packages/core/src/components/WordTable.js +130 -130
  114. package/dist/packages/core/src/components/barChartData.d.ts +16 -1
  115. package/dist/packages/core/src/components/barChartData.js +49 -29
  116. package/dist/packages/core/src/components/barChartTypes.js +8 -8
  117. package/dist/packages/core/src/components/tableTrackList.js +11 -11
  118. package/dist/packages/core/src/components/updateAlertStack.js +28 -28
  119. package/dist/packages/core/src/components/useBarChartControl.js +4 -6
  120. package/dist/packages/core/src/components/useComboboxSuggestions.js +17 -17
  121. package/dist/packages/core/src/components/useToastLifecycle.js +18 -18
  122. package/dist/packages/core/src/components/wordTableLayout.d.ts +17 -4
  123. package/dist/packages/core/src/components/wordTableLayout.js +5 -5
  124. package/dist/packages/core/src/context/I18nProvider.js +6 -6
  125. package/dist/packages/core/src/hooks/focusReturn.js +27 -27
  126. package/dist/packages/core/src/hooks/inputModality.js +13 -13
  127. package/dist/packages/core/src/hooks/touchToggle.js +39 -39
  128. package/dist/packages/core/src/hooks/useAnchoredMenu.js +30 -30
  129. package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +28 -28
  130. package/dist/packages/core/src/hooks/useDocumentTitle.js +10 -10
  131. package/dist/packages/core/src/hooks/useEditableList.js +4 -4
  132. package/dist/packages/core/src/hooks/useFocusTrap.js +2 -2
  133. package/dist/packages/core/src/hooks/useFontScale.js +6 -6
  134. package/dist/packages/core/src/hooks/useIcon.js +7 -7
  135. package/dist/packages/core/src/hooks/useListControls.js +2 -2
  136. package/dist/packages/core/src/hooks/usePageResetOnChange.js +3 -3
  137. package/dist/packages/core/src/hooks/usePasswordChange.js +4 -4
  138. package/dist/packages/core/src/hooks/usePendingPress.js +7 -7
  139. package/dist/packages/core/src/hooks/useRelativeDay.js +4 -4
  140. package/dist/packages/core/src/hooks/useSoundCompassExam.js +6 -6
  141. package/dist/packages/core/src/hooks/useTablePaging.d.ts +0 -11
  142. package/dist/packages/core/src/hooks/useTablePaging.js +29 -34
  143. package/dist/packages/core/src/hooks/useTrainingSessionLog.js +35 -35
  144. package/dist/packages/core/src/hooks/useVisitSchedule.js +4 -4
  145. package/dist/packages/core/src/hooks/useWordAttributeFilter.js +14 -14
  146. package/dist/packages/core/src/hooks/useWordMeanings.js +5 -5
  147. package/dist/packages/core/src/hooks/useWordTrainingFilter.js +7 -7
  148. package/dist/packages/core/src/i18n/locales/en.json +14 -2
  149. package/dist/packages/core/src/i18n/locales/ko.json +13 -1
  150. package/dist/packages/core/src/types/wordSearch.d.ts +9 -0
  151. package/dist/packages/core/src/utils/audioMeter.js +5 -5
  152. package/dist/packages/core/src/utils/audioPeaks.d.ts +1 -1
  153. package/dist/packages/core/src/utils/audioPeaks.js +18 -18
  154. package/dist/packages/core/src/utils/blockGrid.js +4 -4
  155. package/dist/packages/core/src/utils/chunkSkew.js +9 -9
  156. package/dist/packages/core/src/utils/clipboard.js +3 -3
  157. package/dist/packages/core/src/utils/dailySelectionUtils.js +12 -12
  158. package/dist/packages/core/src/utils/date.js +12 -12
  159. package/dist/packages/core/src/utils/devWarn.js +4 -4
  160. package/dist/packages/core/src/utils/deviceKind.d.ts +37 -7
  161. package/dist/packages/core/src/utils/deviceKind.js +53 -10
  162. package/dist/packages/core/src/utils/errorMessage.js +7 -7
  163. package/dist/packages/core/src/utils/filterPanelSummary.js +14 -14
  164. package/dist/packages/core/src/utils/format.js +4 -4
  165. package/dist/packages/core/src/utils/getTrainingData.js +9 -9
  166. package/dist/packages/core/src/utils/imageConfig.js +17 -17
  167. package/dist/packages/core/src/utils/koreanSyllable.js +18 -18
  168. package/dist/packages/core/src/utils/navScroll.js +2 -2
  169. package/dist/packages/core/src/utils/phonemeConvert.d.ts +7 -0
  170. package/dist/packages/core/src/utils/phonemeConvert.js +19 -10
  171. package/dist/packages/core/src/utils/phonemeGroups.js +2 -2
  172. package/dist/packages/core/src/utils/phonemePairSearch.d.ts +1 -0
  173. package/dist/packages/core/src/utils/phonemePairSearch.js +129 -105
  174. package/dist/packages/core/src/utils/phonemePairSearchTypes.d.ts +2 -0
  175. package/dist/packages/core/src/utils/phonemeSectionCodes.d.ts +52 -0
  176. package/dist/packages/core/src/utils/phonemeSectionCodes.js +76 -0
  177. package/dist/packages/core/src/utils/prefetchChunk.js +6 -6
  178. package/dist/packages/core/src/utils/safeHref.js +4 -4
  179. package/dist/packages/core/src/utils/savedWordTables.js +5 -5
  180. package/dist/packages/core/src/utils/speak.js +12 -12
  181. package/dist/packages/core/src/utils/textSort.d.ts +0 -10
  182. package/dist/packages/core/src/utils/textSort.js +38 -48
  183. package/dist/packages/core/src/utils/theme.js +3 -3
  184. package/dist/packages/core/src/utils/trainingItemResults.js +3 -3
  185. package/dist/packages/core/src/utils/ttsConfig.js +2 -2
  186. package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +1 -1
  187. package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +65 -45
  188. package/dist/packages/core/src/utils/wordContrast.js +3 -3
  189. package/dist/packages/core/src/utils/wordPairIndex.js +4 -4
  190. package/dist/packages/core/src/utils/wordTrainingGenerators.js +4 -4
  191. package/dist/packages/core/src/utils/wordTrainingPool.js +15 -15
  192. package/dist/packages/core/src/utils/wordTrainingRuntime.js +6 -6
  193. package/dist/packages/core/src/utils/wornDate.d.ts +3 -3
  194. package/dist/packages/core/src/utils/wornDate.js +14 -14
  195. package/dist/packages/core/src/version.d.ts +1 -1
  196. package/dist/packages/core/src/version.js +1 -1
  197. package/dist/utils.cjs.js +4 -4
  198. package/package.json +1 -1
@@ -26,11 +26,11 @@ export const pctToLevel = (pct) => (pct / 100) * LEVEL_MAX;
26
26
  export const NOISE_RATIO_MAX = 2;
27
27
  /** @public 소음 비율 칸(배). */
28
28
  export const NOISE_RATIO_STEP = 0.1;
29
- const 비율자르기 = (r) => Math.min(NOISE_RATIO_MAX, Math.max(0, r));
29
+ const clampRatio = (r) => Math.min(NOISE_RATIO_MAX, Math.max(0, r));
30
30
  /** 옛 기록에서 옮긴 비율의 범위 — 말귀 noiseRatioOfPct 와 같다(0.1~2). */
31
- const 옛비율최소 = 0.1;
31
+ const LEGACY_RATIO_MIN = 0.1;
32
32
  /* 부동소수 찌꺼기만 턴다(0.30000000000000004 → 0.3) — 0.1 칸으로 반올림하지는 않는다 */
33
- const 찌꺼기털기 = (r) => Math.round(r * 1e6) / 1e6;
33
+ const roundOff = (r) => Math.round(r * 1e6) / 1e6;
34
34
  /**
35
35
  * @public 설정의 소음 비율(배). `noiseRatio` 가 있으면 그것(0~2 로 자름), 없으면 **옛 값에서
36
36
  * 들리던 그대로** 옮긴다 — (level/9) ÷ mainVolume 을 **반올림 없이** 0.1~2 로 조인다(말귀 `noiseRatioOfPct` 와 같은 범위).
@@ -40,26 +40,26 @@ const 찌꺼기털기 = (r) => Math.round(r * 1e6) / 1e6;
40
40
  export function noiseRatioOf(settings) {
41
41
  const { noiseRatio } = settings;
42
42
  if (typeof noiseRatio === 'number' && Number.isFinite(noiseRatio))
43
- return 비율자르기(noiseRatio);
44
- const 소음 = Math.min(LEVEL_MAX, Math.max(0, Number(settings.level) || 0)) / LEVEL_MAX;
45
- const 전체 = Number(settings.mainVolume);
46
- if (!(전체 > 0))
43
+ return clampRatio(noiseRatio);
44
+ const noise = Math.min(LEVEL_MAX, Math.max(0, Number(settings.level) || 0)) / LEVEL_MAX;
45
+ const main = Number(settings.mainVolume);
46
+ if (!(main > 0))
47
47
  return 1;
48
- if (!(소음 > 0))
48
+ if (!(noise > 0))
49
49
  return 0;
50
- return 찌꺼기털기(Math.min(NOISE_RATIO_MAX, Math.max(옛비율최소, 소음 / 전체)));
50
+ return roundOff(Math.min(NOISE_RATIO_MAX, Math.max(LEGACY_RATIO_MIN, noise / main)));
51
51
  }
52
- const 전체소리 = (settings) => Math.min(1, Math.max(0, Number(settings.mainVolume) || 0));
52
+ const mainVolumeOf = (settings) => Math.min(1, Math.max(0, Number(settings.mainVolume) || 0));
53
53
  /** @public 문제 소리 볼륨(0~1) = mainVolume. 비율과 상관없다. */
54
54
  export function effectiveSpeechVolume(settings) {
55
- return 전체소리(settings);
55
+ return mainVolumeOf(settings);
56
56
  }
57
57
  /**
58
58
  * @public 소음이 **들리는 크기**(0~2, 원본 소음 파일 기준) = mainVolume × 비율. 켜짐 여부는 보지
59
59
  * 않는다. 결과 기록·파형이 쓴다. 소음 `<audio>` 에 거는 값은 `noiseElementVolume`.
60
60
  */
61
61
  export function effectiveNoiseVolume(settings) {
62
- return 전체소리(settings) * noiseRatioOf(settings);
62
+ return mainVolumeOf(settings) * noiseRatioOf(settings);
63
63
  }
64
64
  /**
65
65
  * @public 소음 `<audio>` 볼륨(0~1) = min(1, 들리는 크기 ÷ gain). gain 은 소음 파일이 원본보다 몇 배
@@ -76,9 +76,9 @@ export default function NoiseSettingsTooltip({ anchorRect, onClose, hideNoiseRow
76
76
  catch (e) { }
77
77
  }, [noisePlayer]);
78
78
  /* 소음 종류 보기 — **한 곳**이다. 아래 참 줄과 폭 그림자 줄이 같은 배열을 쓴다 */
79
- const 종류옵션 = [
79
+ const kindOptions = [
80
80
  { key: NONE_KEY, label: t('training.noise.type.none', '없음') },
81
81
  ...getNoiseTypeOptions(t).map(opt => ({ key: opt.value, label: opt.label })),
82
82
  ];
83
- return (_jsx(Tooltip, { anchorRect: anchorRect, offset: 12, onClose: onClose, ariaLabel: t('training.trainingSettingsTitle', '훈련 설정'), isMobilePopup: false, closeOnOutsideClick: false, header: _jsxs("div", { className: "noise-settings__header", children: [_jsx("span", { children: t('training.trainingSettingsTitle', '훈련 설정') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, ariaLabel: t('common.close', '닫기'), onClick: onClose })] }), children: _jsxs(Panel, { className: "noise-settings", useBorder: false, labelWidth: 72, children: [_jsx(SliderPanelRow, { title: t('training.playVolumeTitle', '전체 소리'), valueLabel: `${volPct}%`, value: volPct, min: 0, max: 100, step: 10, size: sliderSize, onChange: handlePlayVolumeChange }), _jsx(SliderPanelRow, { title: t('training.playSpeedTitle', '재생 속도'), valueLabel: `${playbackRate.toFixed(2)}x`, value: playbackRate, min: PLAY_SPEED_MIN, max: PLAY_SPEED_MAX, step: PLAY_SPEED_STEP, size: sliderSize, onChange: handlePlaySpeedChange }), !hideNoiseRows && (_jsxs(_Fragment, { children: [_jsx(PanelRow, { variant: "custom", label: t('training.noiseTypeTitle', '소음 종류'), useFullWidth: true, children: _jsx(SegmentedControl, { ariaLabel: t('training.noiseTypeTitle', '소음 종류'), size: 40, shape: "rounded", options: 종류옵션, value: noiseEnabled ? noiseType : NONE_KEY, onChange: handleTypeChange }) }), _jsx(SliderPanelRow, { title: t('training.noiseRatioTitle', '소음 비율'), valueLabel: t('training.noiseRatioValue', '{{ratio}}배', { ratio: ratio.toFixed(1) }), value: ratio, min: 0, max: NOISE_RATIO_MAX, step: NOISE_RATIO_STEP, size: sliderSize, disabled: !noiseEnabled, onChange: handleRatioChange })] })), hideNoiseRows && (_jsx("div", { className: "noise-settings__width-ghost", "aria-hidden": "true", inert: true, children: _jsx(PanelRow, { variant: "custom", label: '\u00a0', useFullWidth: true, children: _jsx(SegmentedControl, { ariaLabel: "width-ghost", size: 40, shape: "rounded", options: 종류옵션, value: NONE_KEY, onChange: () => { } }) }) }))] }) }));
83
+ return (_jsx(Tooltip, { anchorRect: anchorRect, offset: 12, onClose: onClose, ariaLabel: t('training.trainingSettingsTitle', '훈련 설정'), isMobilePopup: false, closeOnOutsideClick: false, header: _jsxs("div", { className: "noise-settings__header", children: [_jsx("span", { children: t('training.trainingSettingsTitle', '훈련 설정') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, ariaLabel: t('common.close', '닫기'), onClick: onClose })] }), children: _jsxs(Panel, { className: "noise-settings", useBorder: false, labelWidth: 72, children: [_jsx(SliderPanelRow, { title: t('training.playVolumeTitle', '전체 소리'), valueLabel: `${volPct}%`, value: volPct, min: 0, max: 100, step: 10, size: sliderSize, onChange: handlePlayVolumeChange }), _jsx(SliderPanelRow, { title: t('training.playSpeedTitle', '재생 속도'), valueLabel: `${playbackRate.toFixed(2)}x`, value: playbackRate, min: PLAY_SPEED_MIN, max: PLAY_SPEED_MAX, step: PLAY_SPEED_STEP, size: sliderSize, onChange: handlePlaySpeedChange }), !hideNoiseRows && (_jsxs(_Fragment, { children: [_jsx(PanelRow, { variant: "custom", label: t('training.noiseTypeTitle', '소음 종류'), useFullWidth: true, children: _jsx(SegmentedControl, { ariaLabel: t('training.noiseTypeTitle', '소음 종류'), size: 40, shape: "rounded", options: kindOptions, value: noiseEnabled ? noiseType : NONE_KEY, onChange: handleTypeChange }) }), _jsx(SliderPanelRow, { title: t('training.noiseRatioTitle', '소음 비율'), valueLabel: t('training.noiseRatioValue', '{{ratio}}배', { ratio: ratio.toFixed(1) }), value: ratio, min: 0, max: NOISE_RATIO_MAX, step: NOISE_RATIO_STEP, size: sliderSize, disabled: !noiseEnabled, onChange: handleRatioChange })] })), hideNoiseRows && (_jsx("div", { className: "noise-settings__width-ghost", "aria-hidden": "true", inert: true, children: _jsx(PanelRow, { variant: "custom", label: '\u00a0', useFullWidth: true, children: _jsx(SegmentedControl, { ariaLabel: "width-ghost", size: 40, shape: "rounded", options: kindOptions, value: NONE_KEY, onChange: () => { } }) }) }))] }) }));
84
84
  }
@@ -31,24 +31,24 @@ export default function TrainingBlock(props) {
31
31
  /* eslint-enable react-hooks/set-state-in-effect */
32
32
  useWindowEvent('speechear.data.updated', () => setDataTick((v) => v + 1));
33
33
  /* 마지막 '더보기' 는 누르면 사라진다 — 초점이 거기 있었으면 새로 붙은 첫 항목으로 옮긴다(안 옮기면 body 로 떨어졌다) */
34
- const 목록Ref = useRef(null);
35
- const 더보기초점Ref = useRef(null);
34
+ const listRef = useRef(null);
35
+ const moreFocusRef = useRef(null);
36
36
  function loadMore() {
37
- const 단추 = document.activeElement;
38
- 더보기초점Ref.current = wasKeyboardPress() && 단추 && 단추.closest?.('.training-block__more') ? visible : null;
37
+ const button = document.activeElement;
38
+ moreFocusRef.current = wasKeyboardPress() && button && button.closest?.('.training-block__more') ? visible : null;
39
39
  setVisible((v) => Math.min(total, v + BTN_COUNT_MAX));
40
40
  }
41
41
  useEffect(() => {
42
- const 첫새 = 더보기초점Ref.current;
43
- if (첫새 == null)
42
+ const firstNew = moreFocusRef.current;
43
+ if (firstNew == null)
44
44
  return;
45
- 더보기초점Ref.current = null;
46
- const 지금 = document.activeElement;
47
- if (지금 && 지금 !== document.body)
45
+ moreFocusRef.current = null;
46
+ const active = document.activeElement;
47
+ if (active && active !== document.body)
48
48
  return;
49
- 목록Ref.current?.querySelectorAll('.training-block-button')[첫새]?.focus();
49
+ listRef.current?.querySelectorAll('.training-block-button')[firstNew]?.focus();
50
50
  }, [visible]);
51
- return (_jsxs(Section, { type: sectionType, title: type?.label, icon: type?.icon, disc: type?.desc, className: "training-block", children: [_jsx("div", { className: "training-list", ref: 목록Ref, children: (itemsAll || []).slice(0, visible).map((item, idx) => {
51
+ return (_jsxs(Section, { type: sectionType, title: type?.label, icon: type?.icon, disc: type?.desc, className: "training-block", children: [_jsx("div", { className: "training-list", ref: listRef, children: (itemsAll || []).slice(0, visible).map((item, idx) => {
52
52
  const itemForBtn = item;
53
53
  const depthForBtn = depthLabel;
54
54
  return (_jsx(TrainingBlockButton, { index: idx, type: type, trainingData: itemForBtn, depth: depthForBtn, renderPopup: renderPopup, completedIds: completedIds, selectedIds: selectedIds, size: size, useDescriptionTooltip: useDescriptionTooltip, onRemove: typeof onRemove === 'function' && selectedIds != null && itemForBtn?.id != null && selectedIds.has(String(itemForBtn.id))
@@ -179,18 +179,18 @@ export default function TrainingBlockButton(props) {
179
179
  const isCompact = size === 'compact';
180
180
  const isSmall = size === 'small';
181
181
  // 유형 이름은 지금 언어로 — 등록된 원본의 한국어 라벨을 써 영어 화면 계획 바구니에 '소음 하 문장 듣기' 가 떴다
182
- const 유형원본 = isSmall && type?.key ? getTrainingTypes().find(x => x.key === type.key) : undefined;
183
- const 유형열쇠 = 유형원본 ? TRAINING_TYPE_I18N_KEY[유형원본.key] : undefined;
184
- const trainingTypeName = 유형원본
185
- ? (유형열쇠 ? t(`trainingType.${유형열쇠}.title`, { defaultValue: 유형원본.label }) : (유형원본.label ?? ''))
182
+ const typeSource = isSmall && type?.key ? getTrainingTypes().find(x => x.key === type.key) : undefined;
183
+ const typeI18nKey = typeSource ? TRAINING_TYPE_I18N_KEY[typeSource.key] : undefined;
184
+ const trainingTypeName = typeSource
185
+ ? (typeI18nKey ? t(`trainingType.${typeI18nKey}.title`, { defaultValue: typeSource.label }) : (typeSource.label ?? ''))
186
186
  : '';
187
187
  // 타입 라벨이 실제로 렌더되는지(타이틀과 같으면 숨김) — 레이아웃 여백 정리에 사용.
188
- const 갈래글 = (!!showTypeLabel && !!type?.label && type.label !== String(itemTitle)) ? String(type.label)
188
+ const typeText = (!!showTypeLabel && !!type?.label && type.label !== String(itemTitle)) ? String(type.label)
189
189
  : (isSmall && !!trainingTypeName && trainingTypeName !== String(itemTitle)) ? trainingTypeName
190
190
  : '';
191
- const typeLabelRendered = !!갈래글;
191
+ const typeLabelRendered = !!typeText;
192
192
  /* 그려지는 수 — 아래 본문과 **같은 값을 본다.** 전에는 JSX 안에서만 셈해서 이름이 못 썼다. */
193
- const 그려지는수 = countLabelOverride !== undefined ? countLabelOverride : countLabel;
193
+ const shownCount = countLabelOverride !== undefined ? countLabelOverride : countLabel;
194
194
  /**
195
195
  * **눈에 보이는 줄 그대로 이름을 짓는다**(말귀 요청 (65) 의 뒷자락, 2026-10-02).
196
196
  *
@@ -209,11 +209,11 @@ export default function TrainingBlockButton(props) {
209
209
  * 때만 넣는다. 그래야 이름과 눈이 어긋나지 않는다(WCAG 2.5.3, 보이는 이름이 읽히는 이름
210
210
  * 안에 들어 있어야 한다). 부르는 쪽이 `ariaLabel` 을 주면 그쪽이 먼저다.
211
211
  */
212
- const 지은이름 = [갈래글, typeof itemTitle === 'string' ? itemTitle : '', 그려지는수]
212
+ const ariaParts = [typeText, typeof itemTitle === 'string' ? itemTitle : '', shownCount]
213
213
  .map(x => String(x ?? '').trim()).filter(Boolean).join(' ');
214
- return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: btnRef, className: `training-block-button${isList ? ' training-block-button--list' : ''}${isRow ? ' training-block-button--row' : ''}${isCompact ? ' training-block-button--compact' : ''}${isSmall ? ' training-block-button--small' : ''}${isCompleted ? ' training-block-button--completed' : ''}${isSelected ? ' training-block-button--selected' : ''}${typeLabelRendered && !isSmall ? ' training-block-button--withType' : ''}${isSmall && !typeLabelRendered ? ' training-block-button--no-type-label' : ''}`, onClick: handleActivate, onPointerEnter: handleBtnHoverOn, onPointerLeave: handleBtnHoverClose, role: "button", tabIndex: 0, onKeyDown: handleKeyDown, "aria-label": typeof ariaLabel === 'string' ? ariaLabel : (지은이름 || undefined),
214
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: btnRef, className: `training-block-button${isList ? ' training-block-button--list' : ''}${isRow ? ' training-block-button--row' : ''}${isCompact ? ' training-block-button--compact' : ''}${isSmall ? ' training-block-button--small' : ''}${isCompleted ? ' training-block-button--completed' : ''}${isSelected ? ' training-block-button--selected' : ''}${typeLabelRendered && !isSmall ? ' training-block-button--withType' : ''}${isSmall && !typeLabelRendered ? ' training-block-button--no-type-label' : ''}`, onClick: handleActivate, onPointerEnter: handleBtnHoverOn, onPointerLeave: handleBtnHoverClose, role: "button", tabIndex: 0, onKeyDown: handleKeyDown, "aria-label": typeof ariaLabel === 'string' ? ariaLabel : (ariaParts || undefined),
215
215
  /* tooltipText 만 주고 useDescriptionTooltip 을 안 켜면 글이 어디에도 안 떴다 — 그때는 title(가리키면 뜨고 낭독기엔 설명)로 준다 */
216
- title: !useDescriptionTooltip && tooltipText ? (Array.isArray(tooltipText) ? tooltipText.filter(Boolean).join('\n') : tooltipText) || undefined : undefined, children: [_jsxs("div", { className: "training-block-button__left", children: [showTypeLabel && type?.label && type.label !== String(itemTitle) && (_jsx("span", { className: "training-block-button__type-label caption-1", children: type.label })), isSmall && trainingTypeName && trainingTypeName !== String(itemTitle) && (_jsx("span", { className: "training-block-button__type-label caption-2", children: trainingTypeName })), _jsx(CategoryIcon, { icon: iconForBtn, label: itemTitle, tooltip: popupIconTooltip, wrapperStyle: wrapperStyle, iconSize: (isSmall || isCompact) ? 'small' : 'medium' }), !hideCheckBadge && (isCompleted || isSelected) && !(onRemove && btnHover) && (_jsx(Badge, { className: onRemove ? 'training-block-button__check--removable' : undefined, type: "dot", icon: "check", position: "top-left", size: "medium", "aria-label": isCompleted ? t('training.completed', '훈련 완료') : t('training.selected', '선택됨') })), onRemove && (_jsx(Badge, { className: `training-block-button__remove${btnHover ? ' is-hover' : ''}`, type: "dot", icon: "remove", position: "top-left", size: "medium", "aria-label": t('common.remove', '제거'), onClick: handleRemoveClick })), _jsx("div", { className: `training-block-button__title ${isCompact ? 'body-3' : isSmall ? 'body-3b' : 'h3'}`, children: String(itemTitle) }), _jsxs("div", { className: "training-block-button__description", children: [그려지는수 ? (_jsx("div", { className: `training-block-button__count ${isSmall ? 'caption-2' : 'caption-1'}`, children: 그려지는수 })) : null, _jsx("div", { className: "training-block-button__chevron", children: _jsx(Icon, { icon: "chevron-right", label: t('training.next', '다음'), className: "training-block-button__chevron-icon", imgClassName: "training-block-button__chevron-icon-image", maskClassName: "training-block-button__chevron-icon-mask", svgClassName: "training-block-button__chevron-icon-svg", size: isSmall ? 12 : 20 }) })] }), ((isList && !isRow) || isSmall) && previewUrls.length > 0 && (_jsx("div", { ref: previewBtnRef, className: "training-block-button__preview", "data-tooltip-ignore": true, onClick: handlePreviewAreaClick, onPointerEnter: handlePreviewHoverOn, onPointerLeave: handlePreviewHoverClose, children: _jsx(Button, { htmlType: "button", size: isSmall ? 28 : 32, type: "dot", icon: playing ? 'pause' : 'play', variant: isSmall ? 'transparent-lightGray' : 'default', useBorder: false, selected: playing,
216
+ title: !useDescriptionTooltip && tooltipText ? (Array.isArray(tooltipText) ? tooltipText.filter(Boolean).join('\n') : tooltipText) || undefined : undefined, children: [_jsxs("div", { className: "training-block-button__left", children: [showTypeLabel && type?.label && type.label !== String(itemTitle) && (_jsx("span", { className: "training-block-button__type-label caption-1", children: type.label })), isSmall && trainingTypeName && trainingTypeName !== String(itemTitle) && (_jsx("span", { className: "training-block-button__type-label caption-2", children: trainingTypeName })), _jsx(CategoryIcon, { icon: iconForBtn, label: itemTitle, tooltip: popupIconTooltip, wrapperStyle: wrapperStyle, iconSize: (isSmall || isCompact) ? 'small' : 'medium' }), !hideCheckBadge && (isCompleted || isSelected) && !(onRemove && btnHover) && (_jsx(Badge, { className: onRemove ? 'training-block-button__check--removable' : undefined, type: "dot", icon: "check", position: "top-left", size: "medium", "aria-label": isCompleted ? t('training.completed', '훈련 완료') : t('training.selected', '선택됨') })), onRemove && (_jsx(Badge, { className: `training-block-button__remove${btnHover ? ' is-hover' : ''}`, type: "dot", icon: "remove", position: "top-left", size: "medium", "aria-label": t('common.remove', '제거'), onClick: handleRemoveClick })), _jsx("div", { className: `training-block-button__title ${isCompact ? 'body-3' : isSmall ? 'body-3b' : 'h3'}`, children: String(itemTitle) }), _jsxs("div", { className: "training-block-button__description", children: [shownCount ? (_jsx("div", { className: `training-block-button__count ${isSmall ? 'caption-2' : 'caption-1'}`, children: shownCount })) : null, _jsx("div", { className: "training-block-button__chevron", children: _jsx(Icon, { icon: "chevron-right", label: t('training.next', '다음'), className: "training-block-button__chevron-icon", imgClassName: "training-block-button__chevron-icon-image", maskClassName: "training-block-button__chevron-icon-mask", svgClassName: "training-block-button__chevron-icon-svg", size: isSmall ? 12 : 20 }) })] }), ((isList && !isRow) || isSmall) && previewUrls.length > 0 && (_jsx("div", { ref: previewBtnRef, className: "training-block-button__preview", "data-tooltip-ignore": true, onClick: handlePreviewAreaClick, onPointerEnter: handlePreviewHoverOn, onPointerLeave: handlePreviewHoverClose, children: _jsx(Button, { htmlType: "button", size: isSmall ? 28 : 32, type: "dot", icon: playing ? 'pause' : 'play', variant: isSmall ? 'transparent-lightGray' : 'default', useBorder: false, selected: playing,
217
217
  /* **칸 이름을 넣는다**(말귀 요청 (54)②, 2026-10-02). 묶음 칸이 스물넷이면
218
218
  낭독기로 단추 목록을 불렀을 때 `미리 듣기` 가 **스물네 번** 선다. 바로 옆
219
219
  '더보기' 는 ㊺ 에서 이미 칸 이름을 달았는데 이쪽만 안 따라왔다 — 고칠 값은
@@ -4,13 +4,13 @@ import { useLayoutEffect, useRef, useState } from 'react';
4
4
  import { cx } from '../../utils/cx';
5
5
  import { balancedColumns, columnsFitting } from '../../utils/blockGrid';
6
6
  import './TrainingBlock.css';
7
- function 자식수(children) {
7
+ function countChildren(children) {
8
8
  let n = 0;
9
- const 세기 = (c) => { if (Array.isArray(c))
10
- c.forEach(세기);
9
+ const walk = (c) => { if (Array.isArray(c))
10
+ c.forEach(walk);
11
11
  else if (c != null && c !== false && c !== true && c !== '')
12
12
  n++; };
13
- 세기(children);
13
+ walk(children);
14
14
  return n;
15
15
  }
16
16
  /**
@@ -30,36 +30,36 @@ function 자식수(children) {
30
30
  */
31
31
  export default function TrainingBlockGrid({ gap = 10, count, className, children }) {
32
32
  const ref = useRef(null);
33
- const [잰것, set잰것] = useState({ 폭: 0, 카드: 0 });
34
- const 장수 = count ?? 자식수(children);
33
+ const [measured, setMeasured] = useState({ width: 0, card: 0 });
34
+ const itemCount = count ?? countChildren(children);
35
35
  useLayoutEffect(() => {
36
36
  const el = ref.current;
37
37
  if (!el)
38
38
  return;
39
- const 재기 = () => {
39
+ const measure = () => {
40
40
  /* 걸어 둔 max-width 가 제 폭을 가리지 않게 잠깐 푼다 */
41
- const 전 = el.style.maxWidth;
41
+ const before = el.style.maxWidth;
42
42
  el.style.maxWidth = 'none';
43
- const 폭 = el.clientWidth;
44
- el.style.maxWidth = 전;
45
- const 첫 = el.firstElementChild;
46
- const 카드 = 첫?.offsetWidth ?? 0;
47
- set잰것(prev => (prev.폭 === 폭 && prev.카드 === 카드 ? prev : { 폭, 카드 }));
43
+ const width = el.clientWidth;
44
+ el.style.maxWidth = before;
45
+ const firstChild = el.firstElementChild;
46
+ const card = firstChild?.offsetWidth ?? 0;
47
+ setMeasured(prev => (prev.width === width && prev.card === card ? prev : { width, card }));
48
48
  };
49
- 재기();
49
+ measure();
50
50
  if (typeof ResizeObserver === 'undefined')
51
51
  return;
52
- const ro = new ResizeObserver(재기);
52
+ const ro = new ResizeObserver(measure);
53
53
  ro.observe(el);
54
54
  return () => ro.disconnect();
55
- }, [장수, gap]);
56
- const 카드폭 = 잰것.카드 > 0 ? 잰것.카드 : 0;
57
- const 한줄 = 카드폭 > 0 ? columnsFitting(잰것.폭, 카드폭, gap) : 0;
55
+ }, [itemCount, gap]);
56
+ const cardWidth = measured.card > 0 ? measured.card : 0;
57
+ const perRow = cardWidth > 0 ? columnsFitting(measured.width, cardWidth, gap) : 0;
58
58
  /* 아직 못 쟀으면(카드가 없거나 첫 틀) 안 건다 — 0 으로 걸면 다 숨는다 */
59
- const 칸수 = 한줄 > 0 ? balancedColumns(장수, 한줄) : 0;
59
+ const columns = perRow > 0 ? balancedColumns(itemCount, perRow) : 0;
60
60
  const style = {
61
61
  '--block-grid-gap': `${gap}px`,
62
- ...(칸수 > 0 ? { '--block-grid-cols': 칸수, '--block-grid-card': `${카드폭}px` } : {}),
62
+ ...(columns > 0 ? { '--block-grid-cols': columns, '--block-grid-card': `${cardWidth}px` } : {}),
63
63
  };
64
- return (_jsx("div", { ref: ref, className: cx('training-block-grid', 칸수 > 0 && 'training-block-grid--measured', className), style: style, "data-cols": 칸수 > 0 ? 칸수 : undefined, children: children }));
64
+ return (_jsx("div", { ref: ref, className: cx('training-block-grid', columns > 0 && 'training-block-grid--measured', className), style: style, "data-cols": columns > 0 ? columns : undefined, children: children }));
65
65
  }
@@ -110,7 +110,19 @@
110
110
 
111
111
  .training-card__description {
112
112
  margin: 0;
113
- color: var(--color-gray);
113
+ /* **곁들이는 줄이라 제목보다 흐리다**(사장님 지시 2026-10-13, 말귀_2 청).
114
+ 제목이 `--color-darkGray`(#434d56)인데 설명이 `--color-gray`(#64748b)로 **두 급 차이가
115
+ 한 급도 안 돼** 제목과 설명이 한 덩어리로 읽혔다. 한 급 내려 제목이 먼저 잡히게 한다.
116
+
117
+ > **흰 바탕 2.53:1 로 본문 기준(4.5)에 못 미치는 것을 알고 그렇게 둔다.** 이 색이 닿는
118
+ > 것은 **문자열 한 줄로 거드는 설명**뿐이다 — 본문이 필요한 자리는 `description` 에
119
+ > `ReactNode` 를 넘기고, 그때는 이 클래스가 **아예 안 붙는다**(`TrainingCard.tsx` 의
120
+ > `typeof description === 'string'` 가름). 그래서 소비처가 열 몇이어도 무게는
121
+ > `WordTableButton` 설명 줄과 같다. 사장님이 그 셈을 보신 뒤 기본값으로 정하셨다.
122
+
123
+ 이 줄을 본문으로 승격하거나 옮길 일이 생기면 **대비를 먼저 다시 본다** —
124
+ `--color-gray`(4.8:1)가 그때의 기본이다. 코어에는 대비를 세는 못이 없어 사람이 본다. */
125
+ color: var(--color-lightGray);
114
126
  text-align: left;
115
127
  /* 폰트는 body-3 토큰(14px/22px) 그대로 사용 — override 제거 */
116
128
  /* 한 줄 말줄임 + 빈 설명도 한 줄 높이 유지(설명 유무로 카드 높이 불변) */
@@ -5,7 +5,7 @@ import './TrainingCard.css';
5
5
  import { useCallback } from 'react';
6
6
  /** @public */
7
7
  export default function TrainingCard({ title, titleLevel = 3, description, topActions, children, footerLeft, footerRight, ariaLabel, onClick, draggable, onDragStart, onDragEnd, className, }) {
8
- const 제목태그 = titleLevel ? `h${titleLevel}` : 'div';
8
+ const TitleTag = titleLevel ? `h${titleLevel}` : 'div';
9
9
  const hasFooter = footerLeft != null || footerRight != null;
10
10
  const hasBody = children != null && children !== false;
11
11
  const handleKeyDown = useCallback((e) => {
@@ -21,7 +21,7 @@ export default function TrainingCard({ title, titleLevel = 3, description, topAc
21
21
  }
22
22
  }, [onClick]);
23
23
  return (_jsxs("div", { className: cx('training-card', onClick ? 'training-card--clickable' : '', className), onClick: onClick, onKeyDown: onClick ? handleKeyDown : undefined, role: onClick ? 'button' : undefined, "aria-label": onClick ? (ariaLabel ?? (typeof title === 'string' ? title : undefined)) : undefined, tabIndex: onClick ? 0 : undefined, draggable: draggable, onDragStart: onDragStart, onDragEnd: onDragEnd, children: [_jsxs("div", { className: "training-card__header", children: [_jsxs("div", { className: "training-card__title-row", children: [typeof title === 'string'
24
- ? _jsx(제목태그, { className: "training-card__title h4", children: title })
24
+ ? _jsx(TitleTag, { className: "training-card__title h4", children: title })
25
25
  : title, topActions != null && (_jsx("div", { className: "training-card__top-actions", onClick: (e) => e.stopPropagation(), children: topActions }))] }), description != null
26
26
  ? (typeof description === 'string'
27
27
  ? _jsx("p", { className: "training-card__description body-3", children: description })
@@ -9,24 +9,24 @@ import { useId } from 'react';
9
9
  /** @public */
10
10
  export default function TrainingCardGroup({ title, titleLevel = 3, count, collapsed = false, onToggle, beforeSort, sort, actions, empty = false, emptyContent, dragOver = false, onDragOver, onDragLeave, onDrop, children, className, }) {
11
11
  const { t } = useTranslation();
12
- const 제목id = useId();
13
- const 제목태그 = titleLevel ? `h${titleLevel}` : 'span';
12
+ const titleId = useId();
13
+ const TitleTag = titleLevel ? `h${titleLevel}` : 'span';
14
14
  /* 접을 수 있는가 — `onToggle` 유무가 그것을 말한다. 새 prop 을 두지 않는다. */
15
- const 접을수있다 = onToggle != null;
16
- const cls = cx('training-card-group', dragOver && 'training-card-group--dragover', collapsed && 'training-card-group--collapsed', 접을수있다 && 'training-card-group--collapsible', className);
15
+ const collapsible = onToggle != null;
16
+ const cls = cx('training-card-group', dragOver && 'training-card-group--dragover', collapsed && 'training-card-group--collapsed', collapsible && 'training-card-group--collapsible', className);
17
17
  return (
18
18
  /* **제목이 이 묶음의 이름이 된다**(말귀 요청 (54)①). 이름 없는 묶음은 지역으로 안
19
19
  잡혀, 안의 `접기`·`정렬` 이 그룹마다 같은 이름으로 서도 가릴 길이 없었다 —
20
20
  `ChartSection` 에 (51) 로 한 것과 같은 자다. 글자 제목일 때만 건다(마디를 가리키므로
21
21
  글이 아닌 제목이면 이름이 안 선다). */
22
- _jsxs("div", { className: cls, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop, role: typeof title === 'string' && title ? 'group' : undefined, "aria-labelledby": typeof title === 'string' && title ? 제목id : undefined, children: [_jsxs("header", { className: "training-card-group__header", children: [접을수있다 && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-right' : 'chevron-down',
22
+ _jsxs("div", { className: cls, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop, role: typeof title === 'string' && title ? 'group' : undefined, "aria-labelledby": typeof title === 'string' && title ? titleId : undefined, children: [_jsxs("header", { className: "training-card-group__header", children: [collapsible && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-right' : 'chevron-down',
23
23
  /* **묶음 이름을 단다** — 그룹이 셋 서면 `접기` 도 셋이라 단추 목록에서 못 가린다.
24
24
  지역(`role="group"`)이 가려 주지 않는다(위 `titleLevel` 머리글). 제목이 글자일
25
25
  때만 — 마디 제목이면 이름을 못 짓는다. */
26
26
  ariaLabel: typeof title === 'string' && title
27
27
  ? (collapsed ? t('common.expandFor', { name: title }) : t('common.collapseFor', { name: title }))
28
28
  : (collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기')), className: "training-card-group__chevron", "aria-expanded": !collapsed, onClick: onToggle })), typeof title === 'string'
29
- ? _jsx(제목태그, { id: 제목id, className: "training-card-group__title h5b", children: title })
29
+ ? _jsx(TitleTag, { id: titleId, className: "training-card-group__title h5b", children: title })
30
30
  : title, count !== undefined && _jsx("span", { className: "training-card-group__count body-3b", children: count }), (beforeSort != null || sort || actions != null) && (_jsxs("span", { className: "training-card-group__actions", onClick: e => e.stopPropagation(), children: [beforeSort, sort && (_jsx(Dropdown, { size: 40, options: sort.options, value: sort.value, placeholder: sort.placeholder, onChange: sort.onChange,
31
31
  /* 접기와 같은 까닭으로 묶음 이름을 단다 — 그룹마다 `정렬` 이 하나씩 선다 */
32
32
  ariaLabel: typeof title === 'string' && title ? t('common.sortFor', { name: title }) : undefined })), actions] }))] }), !collapsed && (empty