@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
@@ -23,13 +23,13 @@ export default function BulkBar({ count, actions, onClear, clearLabel: clearLabe
23
23
  const prevShowRef = useRef(show);
24
24
  useLayoutEffect(() => {
25
25
  const bar = barRef.current;
26
- const 전 = prevShowRef.current;
26
+ const before = prevShowRef.current;
27
27
  prevShowRef.current = show;
28
- if (show && !전) {
28
+ if (show && !before) {
29
29
  const a = typeof document !== 'undefined' ? document.activeElement : null;
30
30
  openerRef.current = a && a !== document.body && !bar?.contains(a) ? a : null;
31
31
  }
32
- else if (!show && 전 && bar && bar.contains(document.activeElement)) {
32
+ else if (!show && before && bar && bar.contains(document.activeElement)) {
33
33
  moveFocusOutside(bar, openerRef.current);
34
34
  }
35
35
  }, [show]);
@@ -38,35 +38,35 @@ export default function BulkBar({ count, actions, onClear, clearLabel: clearLabe
38
38
  문턱이 하나라 **매 프레임 오갔다**(말귀 단어 고름 막대, 칩 7~12개). 되돌릴 때는 "가로일 때의 칸 폭" 에 한 줄로 들어가는지로
39
39
  본다(여유). 가로일 때 잰 칸 폭을 기억해 둔다. */
40
40
  const stackedRef = useRef(false);
41
- const 가로칸폭Ref = useRef(null);
41
+ const rowWidthRef = useRef(null);
42
42
  useEffect(() => {
43
43
  const el = contentRef.current;
44
44
  if (!el)
45
45
  return;
46
- const 두기 = (v) => { stackedRef.current = v; setStacked(v); };
46
+ const setStackedBoth = (v) => { stackedRef.current = v; setStacked(v); };
47
47
  const check = () => {
48
48
  const row = el.firstElementChild ?? el;
49
49
  const kids = Array.from(row.children);
50
50
  if (kids.length < 2) {
51
- 두기(false);
51
+ setStackedBoth(false);
52
52
  return;
53
53
  }
54
54
  const top = kids[0].offsetTop;
55
- const 넘침 = kids.some(k => k.offsetTop !== top);
55
+ const wrapped = kids.some(k => k.offsetTop !== top);
56
56
  if (!stackedRef.current) {
57
- 가로칸폭Ref.current = el.clientWidth;
58
- if (넘침)
59
- 두기(true);
57
+ rowWidthRef.current = el.clientWidth;
58
+ if (wrapped)
59
+ setStackedBoth(true);
60
60
  return;
61
61
  }
62
- if (넘침)
62
+ if (wrapped)
63
63
  return;
64
64
  // 쌓인 채 한 줄에 들어간다 — 가로로 돌렸을 때의 칸에도 들어갈 때만 되돌린다
65
65
  const first = kids[0], last = kids[kids.length - 1];
66
- const 한줄폭 = last.offsetLeft + last.offsetWidth - first.offsetLeft;
67
- const 가로폭 = 가로칸폭Ref.current;
68
- if (가로폭 == null || 한줄폭 <= 가로폭)
69
- 두기(false);
66
+ const rowWidth = last.offsetLeft + last.offsetWidth - first.offsetLeft;
67
+ const storedRowWidth = rowWidthRef.current;
68
+ if (storedRowWidth == null || rowWidth <= storedRowWidth)
69
+ setStackedBoth(false);
70
70
  };
71
71
  check();
72
72
  // ResizeObserver 가 없는 환경(구형 브라우저·SSR·테스트 러너)에서는 생성자 호출이
@@ -77,19 +77,19 @@ export default function BulkBar({ count, actions, onClear, clearLabel: clearLabe
77
77
  obs.observe(el);
78
78
  /* 막대 자체의 폭이 바뀌면(창 크기뿐 아니라 옆 메뉴 접기로 본문 열이 넓어질 때도) 기억한 가로 칸 폭은 옛 것이다 — 버리고 다시
79
79
  잰다. 안 그러면 넘친 순간의 좁은 폭을 들고 있어 넓어져도 쌓임이 영영 안 풀렸다. 단추 배치로는 막대 폭이 안 바뀐다. */
80
- const 막대 = el.parentElement;
81
- let 막대폭 = 막대?.clientWidth ?? 0;
82
- const 막대obs = new ResizeObserver(() => {
83
- const w = 막대?.clientWidth ?? 0;
84
- if (w === 막대폭)
80
+ const bar = el.parentElement;
81
+ let barWidth = bar?.clientWidth ?? 0;
82
+ const barObserver = new ResizeObserver(() => {
83
+ const w = bar?.clientWidth ?? 0;
84
+ if (w === barWidth)
85
85
  return;
86
- 막대폭 = w;
87
- 가로칸폭Ref.current = null;
86
+ barWidth = w;
87
+ rowWidthRef.current = null;
88
88
  check();
89
89
  });
90
- if (막대)
91
- 막대obs.observe(막대);
92
- return () => { obs.disconnect(); 막대obs.disconnect(); };
90
+ if (bar)
91
+ barObserver.observe(bar);
92
+ return () => { obs.disconnect(); barObserver.disconnect(); };
93
93
  }, [content, count]);
94
94
  return (_jsxs("div", { ref: barRef, className: `bulk-bar ${show ? 'bulk-bar--visible' : ''}`,
95
95
  /* 숨은 막대는 CSS 로 투명(opacity 0)할 뿐 DOM 에 그대로 있다 — 예전에는 그 안의
@@ -74,11 +74,11 @@ function ButtonInner(props, forwardedRef) {
74
74
  }
75
75
  }, [htmlType, label]);
76
76
  /* 안쪽 ref 와 부르는 쪽 ref(객체·함수)를 한 요소에 함께 건다 */
77
- const 바깥ref = useRef(forwardedRef);
78
- 바깥ref.current = forwardedRef;
79
- const 요소잇기 = useCallback((el) => {
77
+ const outerRef = useRef(forwardedRef);
78
+ outerRef.current = forwardedRef;
79
+ const attachEl = useCallback((el) => {
80
80
  btnRef.current = el;
81
- const r = 바깥ref.current;
81
+ const r = outerRef.current;
82
82
  if (typeof r === 'function')
83
83
  r(el);
84
84
  else if (r)
@@ -114,14 +114,14 @@ function ButtonInner(props, forwardedRef) {
114
114
  if (prevDisabledRef.current && !disabledProp && btnRef.current) {
115
115
  const btn = btnRef.current;
116
116
  /* 마우스로 잡힌 초점만 푼다 — 키보드로 누른 단추(모두 듣기 등)가 막혔다 풀리는 순간 무조건 blur 해 초점이 body 로 빠졌다 */
117
- let 마우스초점 = false;
117
+ let focusByMouse = false;
118
118
  try {
119
- 마우스초점 = btn.matches(':focus:not(:focus-visible)');
119
+ focusByMouse = btn.matches(':focus:not(:focus-visible)');
120
120
  }
121
121
  catch {
122
- 마우스초점 = false;
122
+ focusByMouse = false;
123
123
  }
124
- if (마우스초점)
124
+ if (focusByMouse)
125
125
  btn.blur();
126
126
  btn.style.pointerEvents = 'none';
127
127
  const id = requestAnimationFrame(() => {
@@ -157,7 +157,7 @@ function ButtonInner(props, forwardedRef) {
157
157
  };
158
158
  /* 떠날 때 비제어 선택 표시를 푼다 — **toggleable 은 제외**(눌러 켠 것이 포인터가 떠나면 꺼졌다).
159
159
  blur 도 이 정리를 쓰지만 소비자의 onMouseLeave 는 부르지 않는다(포커스를 잃은 것은 마우스가 떠난 것이 아니다). */
160
- const 떠남정리 = (e) => {
160
+ const onLeaveCleanup = (e) => {
161
161
  if (typeof selected !== 'boolean' && !toggleable)
162
162
  setInternalSelected(false);
163
163
  e.currentTarget.blur();
@@ -167,14 +167,14 @@ function ButtonInner(props, forwardedRef) {
167
167
  return;
168
168
  if (typeof rest.onMouseLeave === 'function')
169
169
  rest.onMouseLeave(e);
170
- 떠남정리(e);
170
+ onLeaveCleanup(e);
171
171
  };
172
172
  const handleClickInternal = (e) => {
173
173
  if (tooltipOnly && hasTooltip)
174
- 톡토글.touchPress(e.currentTarget);
174
+ touchToggle.touchPress(e.currentTarget);
175
175
  if (disabledProp) {
176
176
  if (hasTooltip)
177
- 톡토글.touchPress(e.currentTarget);
177
+ touchToggle.touchPress(e.currentTarget);
178
178
  /* native disabled 가 아닌 막힌 단추(초점 유지·onDisabledClick)는 click 이 온다 — 폼 안이면 제출되지 않게 기본 동작을 막는다 */
179
179
  e.preventDefault();
180
180
  onDisabledClick?.(e);
@@ -205,7 +205,7 @@ function ButtonInner(props, forwardedRef) {
205
205
  **막힌 단추·tooltipOnly 칩**은 톡으로 열고 닫는다(막힌 단추는 click 이 안 와 pointerup, 포인터조차 안 오면 감싼 요소의 touchend).
206
206
  누르는 단추(삭제·재생 등)는 터치에서 툴팁을 띄우지 않는다 — 누르면 할 일을 하고 끝난다. 마우스 동작은 그대로. */
207
207
  const hasTooltip = Boolean(showTooltipInfo || tooltipDescription);
208
- const 톡토글 = useTouchToggle(tooltipVisible, (next) => { if (next)
208
+ const touchToggle = useTouchToggle(tooltipVisible, (next) => { if (next)
209
209
  showTooltipFn();
210
210
  else
211
211
  hideTooltipFn(); }, btnRef, { enabled: hasTooltip && (disabledProp || tooltipOnly), fallbackOnParent: hasTooltip && disabledProp });
@@ -262,7 +262,7 @@ function ButtonInner(props, forwardedRef) {
262
262
  /* href 가 있고 비활성이 아니면 링크로 렌더한다. */
263
263
  const isLink = !!href && !disabledProp;
264
264
  const Root = (isLink ? 'a' : 'button');
265
- return (_jsxs(_Fragment, { children: [_jsxs(Root, { ...restProps, ref: 요소잇기, ...(isLink
265
+ return (_jsxs(_Fragment, { children: [_jsxs(Root, { ...restProps, ref: attachEl, ...(isLink
266
266
  ? { href, target, rel: rel ?? (target === '_blank' ? 'noreferrer' : undefined) }
267
267
  : { type: htmlType ?? 'button', disabled: disabledProp && !onDisabledClick && !hasFocus }), className: classes, "aria-pressed": ariaPressed, "aria-expanded": typeof ariaExpanded === 'boolean' ? ariaExpanded : rest['aria-expanded'], "aria-controls": ariaControls ?? rest['aria-controls'], "aria-label": ariaLabelString, "aria-disabled": disabledProp && (isLink || !!onDisabledClick || hasFocus) ? true : undefined, "aria-describedby": ariaDescribedBy, style: style, onClick: handleClickInternal, onMouseLeave: handleMouseLeave, onPointerEnter: (e) => {
268
268
  if (!isTouchPointer(e))
@@ -277,7 +277,7 @@ function ButtonInner(props, forwardedRef) {
277
277
  }, onPointerUp: (e) => {
278
278
  // 막힌 단추는 click 이 오지 않는다 — 톡을 뗄 때 뒤집는다
279
279
  if (disabledProp && hasTooltip)
280
- 톡토글.touchPress(e.currentTarget);
280
+ touchToggle.touchPress(e.currentTarget);
281
281
  if (!disabledProp && typeof rest.onPointerUp === 'function')
282
282
  rest.onPointerUp(e);
283
283
  }, onFocus: (e) => {
@@ -290,7 +290,7 @@ function ButtonInner(props, forwardedRef) {
290
290
  }, onBlur: (e) => {
291
291
  setHasFocus(false);
292
292
  if (!disabledProp)
293
- 떠남정리(e);
293
+ onLeaveCleanup(e);
294
294
  hideTooltipFn();
295
295
  if (!disabledProp && typeof rest.onBlur === 'function')
296
296
  rest.onBlur(e);
@@ -15,9 +15,9 @@ export default function Calendar({ visits = [], onVisitClick, onVisitMove, onDay
15
15
  const DOW = useMemo(() => t('calendar.dow', { returnObjects: true }), [t]);
16
16
  /* '오늘' 은 KST 의 날 — 방문 날짜가 KST 달력(toKstYmd)이라, 기기 달력으로 열면 해외 기기에서 오늘 방문이 안 보이는 달에 있고
17
17
  오늘 표시가 다른 날에 붙었다(해외 기기 공식 지원) */
18
- const 오늘Ymd = todayYmdOr(today);
19
- const [year, setYear] = useState(() => Number(오늘Ymd.slice(0, 4)));
20
- const [month, setMonth] = useState(() => Number(오늘Ymd.slice(5, 7)) - 1);
18
+ const todayYmd = todayYmdOr(today);
19
+ const [year, setYear] = useState(() => Number(todayYmd.slice(0, 4)));
20
+ const [month, setMonth] = useState(() => Number(todayYmd.slice(5, 7)) - 1);
21
21
  const [dragOverDate, setDragOverDate] = useState(null);
22
22
  const dragVisitId = useRef(null);
23
23
  const visitsByDate = useMemo(() => {
@@ -54,7 +54,7 @@ export default function Calendar({ visits = [], onVisitClick, onVisitMove, onDay
54
54
  else
55
55
  setMonth(m => m + 1); }, [month]);
56
56
  const fmtDate = (d) => `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
57
- const isToday = (d) => fmtDate(d) === 오늘Ymd;
57
+ const isToday = (d) => fmtDate(d) === todayYmd;
58
58
  return (_jsxs("div", { children: [_jsxs("div", { className: "calendar-nav", children: [_jsx("button", { type: "button", className: "calendar-nav__button", onClick: prev, "aria-label": t('calendar.prevMonth', '이전 달'), children: _jsx(Icon, { icon: "chevron-left", label: "", className: "", svgClassName: "calendar-nav__button-icon", size: 18 }) }), _jsx("span", { className: "calendar-nav__title h5", "aria-live": "polite", children: t('calendar.yearMonth', { year, month: month + 1 }) }), _jsx("button", { type: "button", className: "calendar-nav__button", onClick: next, "aria-label": t('calendar.nextMonth', '다음 달'), children: _jsx(Icon, { icon: "chevron-right", label: "", className: "", svgClassName: "calendar-nav__button-icon", size: 18 }) }), _jsxs("div", { className: "calendar-nav__legend", children: [_jsx("span", { className: "calendar-nav__legend-dot calendar-nav__legend-dot--main" }), _jsx("span", { className: "caption-1", children: t('calendar.legend.visited', '방문') }), _jsx("span", { className: "calendar-nav__legend-dot calendar-nav__legend-dot--sub" }), _jsx("span", { className: "caption-1", children: t('calendar.legend.scheduled', '예약') }), _jsx("span", { className: "calendar-nav__legend-dot calendar-nav__legend-dot--disabled" }), _jsx("span", { className: "caption-1", children: t('calendar.legend.noVisit', '미방문') })] })] }), _jsxs("div", { className: "calendar-grid", children: [DOW.map(d => _jsx("div", { className: "calendar-day-header body-3", children: d }, d)), cells.map((d, i) => {
59
59
  if (d === null)
60
60
  return _jsx("div", { className: "calendar-cell empty" }, `empty-${i}`);
@@ -62,7 +62,7 @@ export default function Calendar({ visits = [], onVisitClick, onVisitMove, onDay
62
62
  const dateKey = fmtDate(day);
63
63
  const dayVisits = visitsByDate[dateKey] ?? [];
64
64
  /* 낭독기에 나가는 '며칠' — 보는 locale 을 따라 글자꼴만 바뀌고 가리키는 날은 KST 하나다(utils/date 머리글) */
65
- const 날짜말 = formatDateLocale(dateKey, i18n.language);
65
+ const dateLabel = formatDateLocale(dateKey, i18n.language);
66
66
  const isDragOver = dragOverDate === dateKey;
67
67
  return (_jsxs("div", { className: `calendar-cell${isToday(day) ? ' today' : ''}${isDragOver ? ' drag-over' : ''}${onDayClick ? ' calendar-cell--clickable' : ''}`, role: onDayClick ? 'button' : undefined, tabIndex: onDayClick ? 0 : undefined, "aria-label": onDayClick ? t('calendar.addOnDate', { date: dateKey }) : undefined, "aria-current": isToday(day) ? 'date' : undefined, onClick: onDayClick ? () => onDayClick(dateKey) : undefined,
68
68
  /* `<div role="button">` 은 브라우저가 Enter·Space 를 눌림으로 바꿔 주지 않는다 */
@@ -93,20 +93,20 @@ export default function Calendar({ visits = [], onVisitClick, onVisitMove, onDay
93
93
  드문 자리가 아니다: 375 에서 칸이 37px, **1189 에서도 111px** 이라 둘 다
94
94
  이 꼴이 선다. 넓은 칸의 이름표는 멀쩡해서 그쪽만 보면 안 보인다.
95
95
  갈래가 하나면 그 갈래 말로, 섞였으면 갈라 센다. */
96
- const 갈래 = [
97
- { key: 'visited', 말: t('calendar.legend.visited', '방문'),
98
- 수: dayVisits.filter(v => (v.status ?? 'visited') === 'visited').length },
99
- { key: 'scheduled', 말: t('calendar.legend.scheduled', '예약'),
100
- 수: dayVisits.filter(v => v.status === 'scheduled').length },
101
- { key: 'noVisit', 말: t('calendar.legend.noVisit', '미방문'),
102
- 수: dayVisits.filter(v => v.status === 'noVisit').length },
103
- ].filter(g => g.수 > 0);
96
+ const kinds = [
97
+ { key: 'visited', label: t('calendar.legend.visited', '방문'),
98
+ count: dayVisits.filter(v => (v.status ?? 'visited') === 'visited').length },
99
+ { key: 'scheduled', label: t('calendar.legend.scheduled', '예약'),
100
+ count: dayVisits.filter(v => v.status === 'scheduled').length },
101
+ { key: 'noVisit', label: t('calendar.legend.noVisit', '미방문'),
102
+ count: dayVisits.filter(v => v.status === 'noVisit').length },
103
+ ].filter(g => g.count > 0);
104
104
  /* 방문뿐인 날은 전 글귀 그대로 간다 — 그 열쇠의 뜻("방문 N건")이 실제로 맞는
105
105
  자리이고, 부르는 쪽이 덮어 둔 글도 그대로 산다. */
106
- const 센말 = 갈래.length === 1 && 갈래[0].key === 'visited'
106
+ const singleKind = kinds.length === 1 && kinds[0].key === 'visited'
107
107
  ? t('calendar.visitCount', { count: dayVisits.length, defaultValue: '방문 {{count}}건' })
108
- : 갈래.map(g => t('calendar.statusCount', { label: g.말, count: g.수, defaultValue: '{{label}} {{count}}건' })).join(' · ');
109
- return (_jsxs("span", { className: "calendar-cell__visit-count caption-1", "aria-label": `${날짜말} ${센말}`, children: [갈래.map(g => (_jsx("span", { className: `calendar-cell__visit-dot calendar-cell__visit-dot--${g.key}`, "aria-hidden": "true" }, g.key))), dayVisits.length] }));
108
+ : kinds.map(g => t('calendar.statusCount', { label: g.label, count: g.count, defaultValue: '{{label}} {{count}}건' })).join(' · ');
109
+ return (_jsxs("span", { className: "calendar-cell__visit-count caption-1", "aria-label": `${dateLabel} ${singleKind}`, children: [kinds.map(g => (_jsx("span", { className: `calendar-cell__visit-dot calendar-cell__visit-dot--${g.key}`, "aria-hidden": "true" }, g.key))), dayVisits.length] }));
110
110
  })(), _jsx("div", { className: "calendar-cell__badges", children: dayVisits.map((v, vi) => (_jsxs("div", { draggable: !!onVisitMove, className: `calendar-visit-badge${v.status === 'noVisit' ? ' calendar-visit-badge--no-visit' : v.status === 'scheduled' ? ' calendar-visit-badge--scheduled' : ''}${onVisitClick ? ' calendar-visit-badge--clickable' : ''}${onVisitMove ? ' calendar-visit-badge--draggable' : ''}`, onDragStart: onVisitMove ? (e) => {
111
111
  dragVisitId.current = v.id;
112
112
  e.dataTransfer.effectAllowed = 'move';
@@ -118,7 +118,7 @@ export default function Calendar({ visits = [], onVisitClick, onVisitMove, onDay
118
118
  // 이름을 대체하므로 낭독기에는 "12시 55분 지한 예약" 만 가고, 어느 날 것인지는
119
119
  // 안 간다. 격자 꼴(`gridcell`)로 날을 함께 읽히는 길도 있지만 그건 화살표
120
120
  // 이동까지 약속하는 것이라, 지금은 이름 안에 날을 적는다(말귀 요청 (60)②).
121
- 날짜말,
121
+ dateLabel,
122
122
  v.time ? formatTimeHM(v.time, t) : null,
123
123
  v.customer,
124
124
  t(v.status === 'noVisit' ? 'calendar.legend.noVisit'
@@ -34,9 +34,9 @@ export default function ChartSection({ title, titleClassName = 'h3', titleLevel
34
34
  const secRef = useRef(null);
35
35
  /* 설명 툴팁 — StatChart 와 같은 결(카드 사각형 기준, 포인터가 나가거나 초점이 빠지면 닫힘).
36
36
  hover 전용이면 키보드로는 볼 수 없어 초점에도 연다. */
37
- const { anchorRect: 설명자리, openAt: 설명열기, close: 설명닫기 } = useHoverAnchor();
38
- const 설명보이기 = useCallback(() => { if (tooltip && secRef.current)
39
- 설명열기(secRef.current); }, [tooltip, 설명열기]);
37
+ const { anchorRect: tipRect, openAt: openTip, close: closeTip } = useHoverAnchor();
38
+ const showTip = useCallback(() => { if (tooltip && secRef.current)
39
+ openTip(secRef.current); }, [tooltip, openTip]);
40
40
  /* 좁은지는 **카드 제 폭** 으로 잰다 — 부르는 쪽이 격자 칸 폭으로 대신 재면 두 칸을 차지하는 카드에서 틀린다.
41
41
  ResizeObserver 가 없는 환경(구형·SSR)에서는 접지 않는다(넓은 쪽이 기본).
42
42
 
@@ -45,29 +45,29 @@ export default function ChartSection({ title, titleClassName = 'h3', titleLevel
45
45
  프레임 동안 머리가 한 줄 늘어 카드가 부풀었다 — 말귀 실측 2026-09-17: 머리 32 → 70 → 32px,
46
46
  섹션 342 → 380 → 342px, 페이지 바닥이 38px 출렁(layout-shift 로 잡힘).
47
47
  뒤따르는 폭 변화는 ResizeObserver 가 그대로 따라간다. */
48
- const [좁음, set좁음] = useState(false);
48
+ const [isNarrow, setIsNarrow] = useState(false);
49
49
  useIsoLayoutEffect(() => {
50
50
  const el = secRef.current;
51
51
  if (!el || !headSegment)
52
52
  return;
53
- const 재기 = (w) => set좁음(w > 0 && w < collapseBelow);
54
- 재기(el.clientWidth);
53
+ const measure = (w) => setIsNarrow(w > 0 && w < collapseBelow);
54
+ measure(el.clientWidth);
55
55
  if (typeof ResizeObserver === 'undefined')
56
56
  return;
57
- const ro = new ResizeObserver(entries => 재기(entries[0].contentRect.width));
57
+ const ro = new ResizeObserver(entries => measure(entries[0].contentRect.width));
58
58
  ro.observe(el);
59
59
  return () => ro.disconnect();
60
60
  }, [headSegment, collapseBelow]);
61
- const [메뉴자리, set메뉴자리] = useState(null);
62
- const 메뉴닫기 = useCallback(() => set메뉴자리(null), []);
63
- const 메뉴열기 = useCallback((e) => {
61
+ const [menuRect, setMenuRect] = useState(null);
62
+ const closeMenu = useCallback(() => setMenuRect(null), []);
63
+ const openMenu = useCallback((e) => {
64
64
  const r = e.currentTarget.getBoundingClientRect();
65
- set메뉴자리(prev => (prev ? null : { left: r.left, right: r.right, top: r.top, bottom: r.bottom, width: r.width, height: r.height, x: r.x, y: r.y }));
65
+ setMenuRect(prev => (prev ? null : { left: r.left, right: r.right, top: r.top, bottom: r.bottom, width: r.width, height: r.height, x: r.x, y: r.y }));
66
66
  }, []);
67
- const 접힘 = !!headSegment && 좁음;
68
- useEffect(() => { if (!접힘)
69
- set메뉴자리(null); }, [접힘]);
70
- const 메뉴항목 = headSegment
67
+ const collapsed = !!headSegment && isNarrow;
68
+ useEffect(() => { if (!collapsed)
69
+ setMenuRect(null); }, [collapsed]);
70
+ const menuItems = headSegment
71
71
  ? [
72
72
  ...headSegment.options.map(o => ({
73
73
  label: o.label,
@@ -82,7 +82,7 @@ export default function ChartSection({ title, titleClassName = 'h3', titleLevel
82
82
  ]
83
83
  : [];
84
84
  /* 누르는 카드 — 머리의 단추·고르개를 눌렀을 때까지 카드가 열리면 안 된다(그 누름은 제 일을 한다) */
85
- const 카드누름 = useCallback((e) => {
85
+ const onCardClick = useCallback((e) => {
86
86
  if (!onClick)
87
87
  return;
88
88
  const t = e.target;
@@ -90,7 +90,7 @@ export default function ChartSection({ title, titleClassName = 'h3', titleLevel
90
90
  return;
91
91
  onClick();
92
92
  }, [onClick]);
93
- const 카드키 = useCallback((e) => {
93
+ const onCardKeyDown = useCallback((e) => {
94
94
  if (!onClick)
95
95
  return;
96
96
  if (e.target !== e.currentTarget)
@@ -100,21 +100,21 @@ export default function ChartSection({ title, titleClassName = 'h3', titleLevel
100
100
  onClick();
101
101
  }
102
102
  }, [onClick]);
103
- const 오른쪽 = typeof headRight === 'function' ? headRight({ collapsed: 접힘 }) : headRight;
103
+ const rightNode = typeof headRight === 'function' ? headRight({ collapsed: collapsed }) : headRight;
104
104
  /* 제목(과 `disc`) 아래 수 — 차트 카드(BarChart·RingChart)가 쓰는 `ChartTotal` 그 꼴을 그대로 쓴다.
105
105
  같은 자리에 두 모양이 생기면 카드마다 눈금이 달라진다(설명 `disc` 를 머리 아래 한 줄로 모은 것과 같은 까닭).
106
106
  0.176.0 까지는 머리 오른쪽에 한 줄로 섰다 — 사용자가 차트 카드의 그 꼴로 정했다(2026-09-17). */
107
- const 수줄 = (totalNum != null || totalLabel) ? (_jsxs(ChartTotal, { label: totalLabel, children: [totalNum != null && _jsx("span", { className: "barchart-total__num h0", children: totalNum }), totalUnit && _jsx("span", { className: "barchart-total__unit h5", children: totalUnit })] })) : null;
107
+ const totalRow = (totalNum != null || totalLabel) ? (_jsxs(ChartTotal, { label: totalLabel, children: [totalNum != null && _jsx("span", { className: "barchart-total__num h0", children: totalNum }), totalUnit && _jsx("span", { className: "barchart-total__unit h5", children: totalUnit })] })) : null;
108
108
  /* 머리 오른쪽에는 **고르개와 `headRight` 만** 남는다 — 고르개가 먼저, 상세보기 단추가 오른쪽 끝(사용자 지시 2026-09-17) */
109
- const 머리오른쪽 = (오른쪽 || headSegment) ? (_jsxs("div", { className: "manager-section__head-right", children: [headSegment && (접힘 ? (_jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "ellipsis", ariaLabel: headSegment.ariaLabel ?? t('common.more', '더보기'), showTooltipInfo: headSegment.ariaLabel ?? t('common.more', '더보기'), "aria-haspopup": "menu", "aria-expanded": !!메뉴자리, onClick: 메뉴열기 })) : (_jsx(SegmentedControl, { size: 32, ariaLabel: headSegment.ariaLabel, options: headSegment.options.map(o => ({ key: o.key, label: o.label, disabled: o.disabled })), value: headSegment.value, onChange: headSegment.onChange }))), 오른쪽] })) : null;
110
- const 제목id = useId();
111
- const 제목태그 = titleLevel ? `h${titleLevel}` : 'div';
112
- return (_jsxs("section", { ref: secRef, className: ['manager-section', inGrid && 'manager-section--grid', onClick && 'manager-section--clickable', className].filter(Boolean).join(' '), onClick: onClick ? 카드누름 : undefined, onKeyDown: onClick ? 카드키 : undefined, onPointerEnter: tooltip ? 설명보이기 : undefined, onPointerLeave: tooltip ? 설명닫기 : undefined, onFocus: tooltip ? 설명보이기 : undefined, onBlur: tooltip ? 설명닫기 : undefined, role: onClick ? 'button' : undefined, tabIndex: onClick ? 0 : undefined, "aria-label": onClick ? (ariaLabel ?? (typeof title === 'string' ? title : undefined)) : undefined, "aria-labelledby": !onClick && title != null ? 제목id : undefined, children: [tooltip && 설명자리 && (_jsx(TooltipInfo, { anchorRect: {
113
- left: 설명자리.left,
114
- top: 설명자리.top + 21,
115
- right: 설명자리.right,
116
- bottom: 설명자리.bottom,
117
- width: 설명자리.width,
118
- height: 설명자리.height,
119
- }, text: tooltip })), headFloat && _jsx(SectionHeadFloat, { children: headFloat }), _jsxs("div", { className: "manager-section__head", children: [_jsxs("div", { className: "manager-section__title-row", children: [icon && _jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "manager-section__icon" }), _jsx(제목태그, { id: 제목id, className: ['manager-section__title', titleClassName].filter(Boolean).join(' '), children: title })] }), 머리오른쪽, disc && _jsx("div", { className: "manager-section__disc body-3", children: disc })] }), (수줄 || subRow) && (_jsxs("div", { className: ['barchart-sub-row', subRowAlign !== 'start' && `barchart-sub-row--${subRowAlign}`].filter(Boolean).join(' '), children: [수줄, subRow] })), children, 메뉴자리 && (_jsx(Tooltip, { anchorRect: 메뉴자리, onClose: 메뉴닫기, ariaLabel: headSegment?.ariaLabel ?? t('tooltip.menu', '메뉴'), children: _jsx(MenuTooltip, { columns: [{ items: 메뉴항목 }], onClose: 메뉴닫기, size: "small" }) }))] }));
109
+ const headRightNode = (rightNode || headSegment) ? (_jsxs("div", { className: "manager-section__head-right", children: [headSegment && (collapsed ? (_jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "ellipsis", ariaLabel: headSegment.ariaLabel ?? t('common.more', '더보기'), showTooltipInfo: headSegment.ariaLabel ?? t('common.more', '더보기'), "aria-haspopup": "menu", "aria-expanded": !!menuRect, onClick: openMenu })) : (_jsx(SegmentedControl, { size: 32, ariaLabel: headSegment.ariaLabel, options: headSegment.options.map(o => ({ key: o.key, label: o.label, disabled: o.disabled })), value: headSegment.value, onChange: headSegment.onChange }))), rightNode] })) : null;
110
+ const titleId = useId();
111
+ const TitleTag = titleLevel ? `h${titleLevel}` : 'div';
112
+ return (_jsxs("section", { ref: secRef, className: ['manager-section', inGrid && 'manager-section--grid', onClick && 'manager-section--clickable', className].filter(Boolean).join(' '), onClick: onClick ? onCardClick : undefined, onKeyDown: onClick ? onCardKeyDown : undefined, onPointerEnter: tooltip ? showTip : undefined, onPointerLeave: tooltip ? closeTip : undefined, onFocus: tooltip ? showTip : undefined, onBlur: tooltip ? closeTip : undefined, role: onClick ? 'button' : undefined, tabIndex: onClick ? 0 : undefined, "aria-label": onClick ? (ariaLabel ?? (typeof title === 'string' ? title : undefined)) : undefined, "aria-labelledby": !onClick && title != null ? titleId : undefined, children: [tooltip && tipRect && (_jsx(TooltipInfo, { anchorRect: {
113
+ left: tipRect.left,
114
+ top: tipRect.top + 21,
115
+ right: tipRect.right,
116
+ bottom: tipRect.bottom,
117
+ width: tipRect.width,
118
+ height: tipRect.height,
119
+ }, text: tooltip })), headFloat && _jsx(SectionHeadFloat, { children: headFloat }), _jsxs("div", { className: "manager-section__head", children: [_jsxs("div", { className: "manager-section__title-row", children: [icon && _jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "manager-section__icon" }), _jsx(TitleTag, { id: titleId, className: ['manager-section__title', titleClassName].filter(Boolean).join(' '), children: title })] }), headRightNode, disc && _jsx("div", { className: "manager-section__disc body-3", children: disc })] }), (totalRow || subRow) && (_jsxs("div", { className: ['barchart-sub-row', subRowAlign !== 'start' && `barchart-sub-row--${subRowAlign}`].filter(Boolean).join(' '), children: [totalRow, subRow] })), children, menuRect && (_jsx(Tooltip, { anchorRect: menuRect, onClose: closeMenu, ariaLabel: headSegment?.ariaLabel ?? t('tooltip.menu', '메뉴'), children: _jsx(MenuTooltip, { columns: [{ items: menuItems }], onClose: closeMenu, size: "small" }) }))] }));
120
120
  }
@@ -6,7 +6,7 @@ import './CountTile.css';
6
6
  import Icon from './Icon';
7
7
  import TrainingTooltip, { TRAINING_TOOLTIP_HOVER_DELAY_MS } from './Tooltip/TrainingTooltip';
8
8
  /** 타일과 툴팁 사이 틈(12px)을 건너는 동안 툴팁을 살려 두는 시간 */
9
- const 건너가는틈_MS = 200;
9
+ const CROSS_GAP_MS = 200;
10
10
  /**
11
11
  * 72×72 정사각 타일. 좌상단에 아이콘, 우하단에 숫자를 배치한다.
12
12
  * 훈련 카드의 타입별 문항 수 표시 등에 사용.
@@ -25,69 +25,69 @@ export default function CountTile({ icon, count, title, plan, tooltip = true, ho
25
25
  * 반응하는 것처럼 안 보였다.** 호버 모양은 `--interactive` 가 맡고 `--clickable` 은 커서만
26
26
  * 지킨다 — 누르는 타일과 얹는 타일이 **같은 호버**를 쓰고 커서만 갈린다.
27
27
  */
28
- const 반응하나 = Boolean(onClick) || Boolean(onMouseEnter) || (tooltip && Boolean(plan));
29
- const cls = cx('count-tile', 반응하나 && 'count-tile--interactive', 반응하나 && hoverAccent && 'count-tile--accent', onClick && 'count-tile--clickable', className);
28
+ const interactive = Boolean(onClick) || Boolean(onMouseEnter) || (tooltip && Boolean(plan));
29
+ const cls = cx('count-tile', interactive && 'count-tile--interactive', interactive && hoverAccent && 'count-tile--accent', onClick && 'count-tile--clickable', className);
30
30
  const ref = useRef(null);
31
31
  const [anchorRect, setAnchorRect] = useState(null);
32
- const 늦춤 = useRef(null);
33
- const 닫힘 = useRef(null);
34
- const 켜짐 = Boolean(anchorRect && plan && tooltip);
35
- const 시계끄기 = useCallback(() => {
36
- if (늦춤.current) {
37
- clearTimeout(늦춤.current);
38
- 늦춤.current = null;
32
+ const openTimer = useRef(null);
33
+ const closeTimer = useRef(null);
34
+ const isOpen = Boolean(anchorRect && plan && tooltip);
35
+ const clearTimers = useCallback(() => {
36
+ if (openTimer.current) {
37
+ clearTimeout(openTimer.current);
38
+ openTimer.current = null;
39
39
  }
40
- if (닫힘.current) {
41
- clearTimeout(닫힘.current);
42
- 닫힘.current = null;
40
+ if (closeTimer.current) {
41
+ clearTimeout(closeTimer.current);
42
+ closeTimer.current = null;
43
43
  }
44
44
  }, []);
45
- const 닫기 = useCallback(() => { 시계끄기(); setAnchorRect(null); }, [시계끄기]);
45
+ const close = useCallback(() => { clearTimers(); setAnchorRect(null); }, [clearTimers]);
46
46
  /* 늦춰서 연다 — 타일이 열 개 붙어 서는 자리라, 지나가기만 해도 뜨면 줄줄이 번쩍인다. */
47
- const 열기 = useCallback(() => {
47
+ const open = useCallback(() => {
48
48
  if (!tooltip || !plan)
49
49
  return;
50
- 시계끄기();
51
- 늦춤.current = setTimeout(() => {
52
- 늦춤.current = null;
50
+ clearTimers();
51
+ openTimer.current = setTimeout(() => {
52
+ openTimer.current = null;
53
53
  setAnchorRect(ref.current?.getBoundingClientRect() ?? null);
54
54
  }, TRAINING_TOOLTIP_HOVER_DELAY_MS);
55
- }, [tooltip, plan, 시계끄기]);
55
+ }, [tooltip, plan, clearTimers]);
56
56
  /**
57
57
  * **바로 닫지 않는다.** 툴팁은 타일에서 12px 떨어져 뜨므로, 타일을 벗어나자마자 닫으면
58
58
  * 그 틈을 건너는 동안 사라져 **툴팁 안의 갈래 아이콘에 마우스를 올릴 수가 없다** — 그
59
59
  * 아이콘에 다시 뜨는 작은 이름표가 이 툴팁을 다는 까닭의 절반이다(사용자 지시 2026-09-23).
60
60
  * 잠깐 두고, 그 사이 툴팁 안으로 들어오면 아래 지킴이가 예약을 무른다.
61
61
  */
62
- const 닫기예약 = useCallback(() => {
63
- if (늦춤.current) {
64
- clearTimeout(늦춤.current);
65
- 늦춤.current = null;
62
+ const scheduleClose = useCallback(() => {
63
+ if (openTimer.current) {
64
+ clearTimeout(openTimer.current);
65
+ openTimer.current = null;
66
66
  }
67
- if (닫힘.current)
67
+ if (closeTimer.current)
68
68
  return;
69
- 닫힘.current = setTimeout(() => { 닫힘.current = null; setAnchorRect(null); }, 건너가는틈_MS);
69
+ closeTimer.current = setTimeout(() => { closeTimer.current = null; setAnchorRect(null); }, CROSS_GAP_MS);
70
70
  }, []);
71
71
  /* 떠난 뒤에 타이머가 터지면 아무도 안 보는 자리에 툴팁이 남는다 */
72
- useEffect(() => () => { 시계끄기(); }, [시계끄기]);
72
+ useEffect(() => () => { clearTimers(); }, [clearTimers]);
73
73
  useEffect(() => { if (!tooltip || !plan)
74
- 닫기(); }, [tooltip, plan, 닫기]);
74
+ close(); }, [tooltip, plan, close]);
75
75
  /* 툴팁이 떠 있는 동안 **포인터가 어디에 있는지** 본다 — 타일이나 툴팁 안이면 살려 두고,
76
76
  둘 다 아니면 닫기를 예약한다. 툴팁은 포털로 body 끝에 나가 타일의 자식이 아니라서,
77
77
  타일에 건 mouseleave 만으로는 "툴팁으로 건너갔다" 를 알 수 없다. */
78
78
  useEffect(() => {
79
- if (!켜짐)
79
+ if (!isOpen)
80
80
  return;
81
- const 살릴자리 = (t) => t instanceof Element && (ref.current?.contains(t) || Boolean(t.closest('.tooltip-training')));
82
- const 보기 = (e) => { if (살릴자리(e.target)) {
83
- 시계끄기();
81
+ const isKeepAlive = (t) => t instanceof Element && (ref.current?.contains(t) || Boolean(t.closest('.tooltip-training')));
82
+ const onPointerOut = (e) => { if (isKeepAlive(e.target)) {
83
+ clearTimers();
84
84
  }
85
85
  else
86
- 닫기예약(); };
87
- document.addEventListener('pointerover', 보기, true);
88
- return () => document.removeEventListener('pointerover', 보기, true);
89
- }, [켜짐, 시계끄기, 닫기예약]);
90
- return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: ref, className: cls, onClick: onClick ? (e) => { 닫기(); onClick(e); } : undefined,
86
+ scheduleClose(); };
87
+ document.addEventListener('pointerover', onPointerOut, true);
88
+ return () => document.removeEventListener('pointerover', onPointerOut, true);
89
+ }, [isOpen, clearTimers, scheduleClose]);
90
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: ref, className: cls, onClick: onClick ? (e) => { close(); onClick(e); } : undefined,
91
91
  /* role="button" + tabIndex 만 있고 키 처리가 없으면, 키보드 사용자는 여기 멈추고도
92
92
  누를 수가 없다 — `<div>` 는 브라우저가 Enter·Space 를 대신 처리해 주지 않는다.
93
93
  Space 의 기본 스크롤도 막는다(2026-08-28, Badge·Accordion 과 같은 처리). */
@@ -107,7 +107,7 @@ export default function CountTile({ icon, count, title, plan, tooltip = true, ho
107
107
  (pointerdown 에서 닫는 것으로는 못 막는다 — 그 이벤트가 mouseenter 보다 **먼저** 와서,
108
108
  닫은 뒤에 다시 열린다.) */
109
109
  onPointerEnter: (e) => { if (e.pointerType === 'touch' || e.pointerType === 'pen')
110
- 닫기();
110
+ close();
111
111
  else
112
- 열기(); }, onMouseEnter: onMouseEnter, onMouseLeave: (e) => { 닫기예약(); onMouseLeave?.(e); }, role: onClick ? 'button' : 'img', tabIndex: onClick ? 0 : undefined, "aria-label": `${title} ${count}`, children: [_jsx(Icon, { icon: icon, size: 20, className: "count-tile__icon" }), _jsx("span", { className: "count-tile__count h5", "aria-hidden": "true", children: count })] }), anchorRect && plan && _jsx(TrainingTooltip, { plan: plan, anchorRect: anchorRect, onClose: 닫기 })] }));
112
+ open(); }, onMouseEnter: onMouseEnter, onMouseLeave: (e) => { scheduleClose(); onMouseLeave?.(e); }, role: onClick ? 'button' : 'img', tabIndex: onClick ? 0 : undefined, "aria-label": `${title} ${count}`, children: [_jsx(Icon, { icon: icon, size: 20, className: "count-tile__icon" }), _jsx("span", { className: "count-tile__count h5", "aria-hidden": "true", children: count })] }), anchorRect && plan && _jsx(TrainingTooltip, { plan: plan, anchorRect: anchorRect, onClose: close })] }));
113
113
  }