@ihabdevteam/core 0.321.0 → 0.323.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/CHANGELOG.md +617 -0
  2. package/dist/components.cjs.js +183 -183
  3. package/dist/components.esm.css +1 -1
  4. package/dist/hooks.cjs.js +3 -3
  5. package/dist/i18n.cjs.js +3 -3
  6. package/dist/ihabdevteam-core.cjs.js +187 -187
  7. package/dist/ihabdevteam-core.d.ts +108 -66
  8. package/dist/packages/core/src/components/Accordion.js +3 -3
  9. package/dist/packages/core/src/components/Alert.js +2 -2
  10. package/dist/packages/core/src/components/BarChart.js +120 -119
  11. package/dist/packages/core/src/components/BottomNav.js +18 -18
  12. package/dist/packages/core/src/components/BulkBar.js +25 -25
  13. package/dist/packages/core/src/components/Button.js +16 -16
  14. package/dist/packages/core/src/components/Calendar.js +17 -17
  15. package/dist/packages/core/src/components/ChartSection.js +30 -30
  16. package/dist/packages/core/src/components/CountTile.js +38 -38
  17. package/dist/packages/core/src/components/DatePicker.js +59 -59
  18. package/dist/packages/core/src/components/Dialog.js +14 -14
  19. package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.js +8 -8
  20. package/dist/packages/core/src/components/FilterPanel.js +6 -6
  21. package/dist/packages/core/src/components/FilterSectionInfo.js +6 -6
  22. package/dist/packages/core/src/components/GameIntro.js +1 -1
  23. package/dist/packages/core/src/components/HearingDeviceInfo.d.ts +5 -5
  24. package/dist/packages/core/src/components/HearingDeviceInfo.js +2 -2
  25. package/dist/packages/core/src/components/Icon.js +2 -2
  26. package/dist/packages/core/src/components/ItemActions.js +16 -16
  27. package/dist/packages/core/src/components/LandingHeader.js +6 -6
  28. package/dist/packages/core/src/components/LineChart.js +24 -24
  29. package/dist/packages/core/src/components/Loader.js +5 -5
  30. package/dist/packages/core/src/components/NoticeBox.js +3 -3
  31. package/dist/packages/core/src/components/PageTitle.js +2 -2
  32. package/dist/packages/core/src/components/Pagination.js +4 -4
  33. package/dist/packages/core/src/components/Panel.js +2 -2
  34. package/dist/packages/core/src/components/PanelRow.d.ts +2 -4
  35. package/dist/packages/core/src/components/PanelRow.js +8 -15
  36. package/dist/packages/core/src/components/PassCard/PassCard.js +9 -9
  37. package/dist/packages/core/src/components/PassCard/passCardProduct.js +2 -2
  38. package/dist/packages/core/src/components/PaymentPlan.js +5 -5
  39. package/dist/packages/core/src/components/Popup/ConfirmPopup.js +29 -29
  40. package/dist/packages/core/src/components/Popup/CouponInputPopup.js +2 -2
  41. package/dist/packages/core/src/components/Popup/DeviceListPopup.d.ts +4 -8
  42. package/dist/packages/core/src/components/Popup/DeviceListPopup.js +1 -1
  43. package/dist/packages/core/src/components/Popup/DeviceSideTable.js +19 -26
  44. package/dist/packages/core/src/components/Popup/ListPopup.js +6 -6
  45. package/dist/packages/core/src/components/Popup/LoadTablePopup.js +10 -10
  46. package/dist/packages/core/src/components/Popup/Popup.js +27 -27
  47. package/dist/packages/core/src/components/Popup/SaveTablePopup.js +5 -5
  48. package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +4 -4
  49. package/dist/packages/core/src/components/Popup/customerDetailUtils.js +18 -18
  50. package/dist/packages/core/src/components/Popup/useDiscardGuard.js +4 -4
  51. package/dist/packages/core/src/components/ProgressIndicator.js +5 -5
  52. package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +16 -16
  53. package/dist/packages/core/src/components/Rating.js +20 -20
  54. package/dist/packages/core/src/components/Report/ReportDocument.js +21 -21
  55. package/dist/packages/core/src/components/RingChart.js +5 -5
  56. package/dist/packages/core/src/components/SectionHeadFloat.js +15 -15
  57. package/dist/packages/core/src/components/SegmentedControl.js +34 -34
  58. package/dist/packages/core/src/components/SideNav.js +37 -37
  59. package/dist/packages/core/src/components/Slider.js +2 -2
  60. package/dist/packages/core/src/components/SoundCompass.js +5 -5
  61. package/dist/packages/core/src/components/Table.js +257 -98
  62. package/dist/packages/core/src/components/Tabs.js +12 -12
  63. package/dist/packages/core/src/components/TextField.js +34 -34
  64. package/dist/packages/core/src/components/Toast.js +6 -6
  65. package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +4 -4
  66. package/dist/packages/core/src/components/Tooltip/Tooltip.js +22 -22
  67. package/dist/packages/core/src/components/Tooltip/TrainingTooltip.js +4 -4
  68. package/dist/packages/core/src/components/TopBanner.js +28 -28
  69. package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +11 -11
  70. package/dist/packages/core/src/components/Training/CategoryIcon.js +2 -2
  71. package/dist/packages/core/src/components/Training/NoisePlayerContext.d.ts +7 -4
  72. package/dist/packages/core/src/components/Training/NoisePlayerContext.js +12 -12
  73. package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +2 -2
  74. package/dist/packages/core/src/components/Training/TrainingBlock.js +12 -12
  75. package/dist/packages/core/src/components/Training/TrainingBlockButton.js +10 -10
  76. package/dist/packages/core/src/components/Training/TrainingBlockGrid.js +21 -21
  77. package/dist/packages/core/src/components/Training/TrainingCard.css +13 -1
  78. package/dist/packages/core/src/components/Training/TrainingCard.js +2 -2
  79. package/dist/packages/core/src/components/Training/TrainingCardGroup.js +6 -6
  80. package/dist/packages/core/src/components/Training/TrainingPagePopup.js +83 -83
  81. package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +17 -17
  82. package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +128 -128
  83. package/dist/packages/core/src/components/Training/TrainingStepCard.js +18 -18
  84. package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +4 -4
  85. package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +12 -12
  86. package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +13 -13
  87. package/dist/packages/core/src/components/Training/TrainingStepTitle.js +4 -4
  88. package/dist/packages/core/src/components/Training/TrainingWordCard.js +17 -17
  89. package/dist/packages/core/src/components/Training/WordTablePopup.js +5 -5
  90. package/dist/packages/core/src/components/Training/audioPlaybackRules.js +8 -8
  91. package/dist/packages/core/src/components/Training/audioTimeline.js +25 -25
  92. package/dist/packages/core/src/components/Training/oneShotSound.d.ts +1 -1
  93. package/dist/packages/core/src/components/Training/oneShotSound.js +14 -14
  94. package/dist/packages/core/src/components/Training/stepAnswerable.d.ts +2 -2
  95. package/dist/packages/core/src/components/Training/trainingMissStats.js +22 -22
  96. package/dist/packages/core/src/components/Training/trainingPopupUtils.js +7 -7
  97. package/dist/packages/core/src/components/Training/useAudioPreview.js +15 -15
  98. package/dist/packages/core/src/components/Training/useFloatingFocus.js +14 -14
  99. package/dist/packages/core/src/components/Training/useNoiseFade.js +22 -22
  100. package/dist/packages/core/src/components/Training/useObjectUrls.js +2 -2
  101. package/dist/packages/core/src/components/Training/usePlaybackSequencer.d.ts +2 -2
  102. package/dist/packages/core/src/components/Training/usePlaybackSequencer.js +23 -23
  103. package/dist/packages/core/src/components/Training/useProgressTicker.js +33 -33
  104. package/dist/packages/core/src/components/Training/useTTSPlayback.js +17 -17
  105. package/dist/packages/core/src/components/Training/useWaveReady.js +11 -11
  106. package/dist/packages/core/src/components/UpdateAlert.js +6 -6
  107. package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +7 -1
  108. package/dist/packages/core/src/components/Word/TableFilterPanel.js +3 -3
  109. package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +18 -18
  110. package/dist/packages/core/src/components/Word/WordTableBlock.js +28 -28
  111. package/dist/packages/core/src/components/Word/WordTableButton.css +16 -2
  112. package/dist/packages/core/src/components/Word/WordTableThumbnail.css +6 -1
  113. package/dist/packages/core/src/components/Word/generatedDraft.js +14 -14
  114. package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -7
  115. package/dist/packages/core/src/components/Word/wordScoreStats.js +17 -17
  116. package/dist/packages/core/src/components/Word/wordTablePrint.js +10 -10
  117. package/dist/packages/core/src/components/Word/wordTableRecommend.js +13 -13
  118. package/dist/packages/core/src/components/Word/wordTableReview.js +28 -28
  119. package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +8 -11
  120. package/dist/packages/core/src/components/Word/wordTableSettings.js +48 -36
  121. package/dist/packages/core/src/components/WordTable.js +130 -130
  122. package/dist/packages/core/src/components/barChartData.d.ts +16 -1
  123. package/dist/packages/core/src/components/barChartData.js +49 -29
  124. package/dist/packages/core/src/components/barChartTypes.js +8 -8
  125. package/dist/packages/core/src/components/tableTrackList.js +11 -11
  126. package/dist/packages/core/src/components/updateAlertStack.js +28 -28
  127. package/dist/packages/core/src/components/useBarChartControl.js +4 -6
  128. package/dist/packages/core/src/components/useComboboxSuggestions.js +17 -17
  129. package/dist/packages/core/src/components/useToastLifecycle.js +18 -18
  130. package/dist/packages/core/src/components/wordTableLayout.d.ts +22 -7
  131. package/dist/packages/core/src/components/wordTableLayout.js +5 -5
  132. package/dist/packages/core/src/context/I18nProvider.js +6 -6
  133. package/dist/packages/core/src/hooks/focusReturn.js +27 -27
  134. package/dist/packages/core/src/hooks/inputModality.d.ts +1 -1
  135. package/dist/packages/core/src/hooks/inputModality.js +14 -14
  136. package/dist/packages/core/src/hooks/touchToggle.js +39 -39
  137. package/dist/packages/core/src/hooks/useAnchoredMenu.js +30 -30
  138. package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +28 -28
  139. package/dist/packages/core/src/hooks/useDeviceManagement.d.ts +5 -17
  140. package/dist/packages/core/src/hooks/useDeviceManagement.js +20 -8
  141. package/dist/packages/core/src/hooks/useDocumentTitle.js +10 -10
  142. package/dist/packages/core/src/hooks/useEditableList.js +4 -4
  143. package/dist/packages/core/src/hooks/useFocusTrap.js +2 -2
  144. package/dist/packages/core/src/hooks/useFontScale.js +6 -6
  145. package/dist/packages/core/src/hooks/useIcon.js +7 -7
  146. package/dist/packages/core/src/hooks/useListControls.js +2 -2
  147. package/dist/packages/core/src/hooks/usePageResetOnChange.d.ts +3 -3
  148. package/dist/packages/core/src/hooks/usePageResetOnChange.js +6 -6
  149. package/dist/packages/core/src/hooks/usePasswordChange.js +4 -4
  150. package/dist/packages/core/src/hooks/usePendingPress.js +7 -7
  151. package/dist/packages/core/src/hooks/useRelativeDay.js +4 -4
  152. package/dist/packages/core/src/hooks/useSoundCompassExam.js +6 -6
  153. package/dist/packages/core/src/hooks/useTablePaging.d.ts +7 -7
  154. package/dist/packages/core/src/hooks/useTablePaging.js +40 -34
  155. package/dist/packages/core/src/hooks/useTrainingSessionLog.js +35 -35
  156. package/dist/packages/core/src/hooks/useVisitSchedule.js +4 -4
  157. package/dist/packages/core/src/hooks/useWordAttributeFilter.js +14 -14
  158. package/dist/packages/core/src/hooks/useWordMeanings.js +5 -5
  159. package/dist/packages/core/src/hooks/useWordTrainingFilter.d.ts +3 -1
  160. package/dist/packages/core/src/hooks/useWordTrainingFilter.js +14 -12
  161. package/dist/packages/core/src/i18n/locales/en.json +13 -2
  162. package/dist/packages/core/src/i18n/locales/ko.json +12 -1
  163. package/dist/packages/core/src/types/wordSearch.d.ts +9 -0
  164. package/dist/packages/core/src/utils/audioMeter.js +5 -5
  165. package/dist/packages/core/src/utils/audioPeaks.d.ts +1 -1
  166. package/dist/packages/core/src/utils/audioPeaks.js +18 -18
  167. package/dist/packages/core/src/utils/blockGrid.js +4 -4
  168. package/dist/packages/core/src/utils/chunkSkew.js +9 -9
  169. package/dist/packages/core/src/utils/clipboard.js +3 -3
  170. package/dist/packages/core/src/utils/dailySelectionUtils.js +12 -12
  171. package/dist/packages/core/src/utils/date.js +12 -12
  172. package/dist/packages/core/src/utils/devWarn.d.ts +2 -2
  173. package/dist/packages/core/src/utils/devWarn.js +7 -7
  174. package/dist/packages/core/src/utils/deviceKind.d.ts +37 -7
  175. package/dist/packages/core/src/utils/deviceKind.js +53 -10
  176. package/dist/packages/core/src/utils/errorMessage.js +7 -7
  177. package/dist/packages/core/src/utils/filterPanelSummary.d.ts +1 -1
  178. package/dist/packages/core/src/utils/filterPanelSummary.js +16 -16
  179. package/dist/packages/core/src/utils/format.js +4 -4
  180. package/dist/packages/core/src/utils/getTrainingData.js +9 -9
  181. package/dist/packages/core/src/utils/imageConfig.js +17 -17
  182. package/dist/packages/core/src/utils/koreanSyllable.js +18 -18
  183. package/dist/packages/core/src/utils/navScroll.d.ts +2 -2
  184. package/dist/packages/core/src/utils/navScroll.js +6 -6
  185. package/dist/packages/core/src/utils/phonemeConvert.d.ts +7 -0
  186. package/dist/packages/core/src/utils/phonemeConvert.js +19 -10
  187. package/dist/packages/core/src/utils/phonemeGroups.js +2 -2
  188. package/dist/packages/core/src/utils/phonemePairSearch.d.ts +1 -0
  189. package/dist/packages/core/src/utils/phonemePairSearch.js +118 -102
  190. package/dist/packages/core/src/utils/phonemeSectionCodes.d.ts +52 -0
  191. package/dist/packages/core/src/utils/phonemeSectionCodes.js +76 -0
  192. package/dist/packages/core/src/utils/prefetchChunk.js +6 -6
  193. package/dist/packages/core/src/utils/pressable.d.ts +1 -1
  194. package/dist/packages/core/src/utils/pressable.js +3 -3
  195. package/dist/packages/core/src/utils/safeHref.js +4 -4
  196. package/dist/packages/core/src/utils/savedWordTables.js +5 -5
  197. package/dist/packages/core/src/utils/sessionTab.d.ts +7 -7
  198. package/dist/packages/core/src/utils/sessionTab.js +9 -9
  199. package/dist/packages/core/src/utils/speak.js +12 -12
  200. package/dist/packages/core/src/utils/textSort.d.ts +5 -5
  201. package/dist/packages/core/src/utils/textSort.js +47 -47
  202. package/dist/packages/core/src/utils/theme.js +3 -3
  203. package/dist/packages/core/src/utils/trainingItemResults.js +3 -3
  204. package/dist/packages/core/src/utils/ttsConfig.js +2 -2
  205. package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +2 -2
  206. package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +48 -48
  207. package/dist/packages/core/src/utils/wordContrast.js +3 -3
  208. package/dist/packages/core/src/utils/wordPairIndex.js +4 -4
  209. package/dist/packages/core/src/utils/wordTrainingGenerators.js +4 -4
  210. package/dist/packages/core/src/utils/wordTrainingPool.js +15 -15
  211. package/dist/packages/core/src/utils/wordTrainingRuntime.js +6 -6
  212. package/dist/packages/core/src/utils/wornDate.d.ts +3 -3
  213. package/dist/packages/core/src/utils/wornDate.js +14 -14
  214. package/dist/packages/core/src/version.d.ts +1 -1
  215. package/dist/packages/core/src/version.js +1 -1
  216. package/dist/utils.cjs.js +4 -4
  217. package/package.json +1 -1
@@ -31,7 +31,7 @@ function formatDisplay(iso) {
31
31
  }
32
32
  /* 연월 이름은 `연월이름` 으로 받는다 — 예전에는 `${y}년 ${m + 1}월` 로 박혀 있어 영어 화면에서도
33
33
  한국어 연월이 떴다. 부르는 쪽이 사전(`calendar.yearMonth`, Calendar 와 같은 열쇠)을 넘긴다. */
34
- function buildYMOptions(viewY, 연월이름, min, max) {
34
+ function buildYMOptions(viewY, ymLabel, min, max) {
35
35
  const startY = min ? +min.split('-')[0] : viewY - 10;
36
36
  const endY = max ? +max.split('-')[0] : viewY + 10;
37
37
  const opts = [];
@@ -42,7 +42,7 @@ function buildYMOptions(viewY, 연월이름, min, max) {
42
42
  continue;
43
43
  if (max && ym > max.slice(0, 7))
44
44
  continue;
45
- opts.push({ value: ym, label: 연월이름(y, m + 1) });
45
+ opts.push({ value: ym, label: ymLabel(y, m + 1) });
46
46
  }
47
47
  }
48
48
  return opts;
@@ -57,21 +57,21 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
57
57
  진작 `calendar.dow` 를 쓰고 있었고, 사전에는 영어(`Sun`·`Mon`…)도 들어 있었다. */
58
58
  const DOW = useMemo(() => t('calendar.dow', { returnObjects: true }), [t]);
59
59
  // 오늘 표시·열 때 포커스는 KST 의 날 — 기기 달력이면 해외 기기에서 하루 어긋나 min=오늘 달력이 앞 달로 열렸다
60
- const 오늘ISO = todayYmdOr(today);
60
+ const todayIso = todayYmdOr(today);
61
61
  const { viewY, viewM, prevMonth, nextMonth, setViewYM } = useCalendarMonth(value, today);
62
62
  const panelId = useId();
63
63
  const computeStyle = useCallback((rect, trigger) => {
64
64
  const vh = window.innerHeight;
65
65
  const vw = window.innerWidth;
66
66
  /* 판의 실제 크기 — 열린 뒤에 재므로 있다. 없으면(첫 계산 전) 어림값 */
67
- const 판 = typeof document !== 'undefined' ? document.getElementById(panelId) : null;
67
+ const panel = typeof document !== 'undefined' ? document.getElementById(panelId) : null;
68
68
  /* **우리가 얹은 자리에 흔들리지 않는 높이를 쓴다.**
69
69
  `offsetHeight` 만 보면 우리가 방금 얹은 `top`/`bottom` 이 상자를 눌렀을 때 그
70
70
  눌린 값을 되읽는다 — 그 값으로 다시 자리를 잡고, 자리가 다시 높이를 바꾸어
71
71
  두 꼴을 오간다(실측: 306 ↔ 26). `scrollHeight` 는 눌려도 **안엣것의 높이**를
72
72
  그대로 내놓으므로 둘 중 큰 쪽을 쓴다. 판이 아직 없으면 어림값. */
73
- const dropdownH = Math.max(판?.offsetHeight || 0, 판?.scrollHeight || 0) || 300;
74
- const 판폭 = Math.max(rect.width, 판?.offsetWidth || 0, 260);
73
+ const dropdownH = Math.max(panel?.offsetHeight || 0, panel?.scrollHeight || 0) || 300;
74
+ const panelWidth = Math.max(rect.width, panel?.offsetWidth || 0, 260);
75
75
  const spaceBelow = vh - rect.bottom;
76
76
  const spaceAbove = rect.top;
77
77
  /* **아래가 모자라면 넓은 쪽으로 편다**(말귀 요청 ㊵, 2026-10-01).
@@ -82,11 +82,11 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
82
82
  const above = spaceBelow < dropdownH && spaceAbove > spaceBelow;
83
83
  /* **창 안으로 조인다** — 스크롤을 따라 다시 잴 때 트리거가 화면 밖으로 나가면 달력이 창 위·아래 밖까지 따라가
84
84
  고를 수 없었다. 판이 창보다 크면 위 여백에 붙인다. */
85
- const 여백 = 8;
86
- const 조이기 = (v, 최대) => Math.min(Math.max(여백, v), Math.max(여백, 최대));
85
+ const margin = 8;
86
+ const clamp = (v, upper) => Math.min(Math.max(margin, v), Math.max(margin, upper));
87
87
  return {
88
88
  position: 'fixed',
89
- left: 조이기(rect.left, vw - 판폭 - 여백),
89
+ left: clamp(rect.left, vw - panelWidth - margin),
90
90
  width: rect.width,
91
91
  minWidth: 260,
92
92
  /* **둘 다 적는다 — 쓰는 쪽만 적으면 안 된다.**
@@ -95,8 +95,8 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
95
95
  `top` 이 이기고 높이는 그 둘 사이로 눌린다 — 판이 26px 으로 쪼그라들어
96
96
  화면 밖에 섰다(말귀 실측 2026-10-01). `auto` 로 **지운다.** */
97
97
  ...(above
98
- ? { top: 'auto', bottom: 조이기(vh - rect.top + 6, vh - dropdownH - 여백) }
99
- : { bottom: 'auto', top: 조이기(rect.bottom + 6, vh - dropdownH - 여백) }),
98
+ ? { top: 'auto', bottom: clamp(vh - rect.top + 6, vh - dropdownH - margin) }
99
+ : { bottom: 'auto', top: clamp(rect.bottom + 6, vh - dropdownH - margin) }),
100
100
  zIndex: anchoredMenuZIndex(trigger),
101
101
  };
102
102
  }, [panelId]);
@@ -122,17 +122,17 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
122
122
  useEffect(() => {
123
123
  if (!open)
124
124
  return;
125
- const 판 = menuRef.current;
126
- const 갈곳 = 판?.querySelector('.date-picker__day--selected:not(:disabled)') ??
127
- 판?.querySelector('.date-picker__day--today:not(:disabled)') ??
125
+ const panel = menuRef.current;
126
+ const focusTo = panel?.querySelector('.date-picker__day--selected:not(:disabled)') ??
127
+ panel?.querySelector('.date-picker__day--today:not(:disabled)') ??
128
128
  /* 오늘이 min/max 밖이라 꺼져 있으면 focus() 가 실패해 트리거에 남았다 — 켜진 첫 날로 */
129
- 판?.querySelector('.date-picker__day:not(:disabled)') ??
130
- 판?.querySelector(FOCUSABLE_SELECTOR) ??
131
- 판;
132
- 갈곳?.focus?.();
129
+ panel?.querySelector('.date-picker__day:not(:disabled)') ??
130
+ panel?.querySelector(FOCUSABLE_SELECTOR) ??
131
+ panel;
132
+ focusTo?.focus?.();
133
133
  return () => {
134
- const 지금 = document.activeElement;
135
- if (!지금 || 지금 === document.body)
134
+ const active = document.activeElement;
135
+ if (!active || active === document.body)
136
136
  triggerRef.current?.focus?.();
137
137
  };
138
138
  }, [open, menuRef, triggerRef]);
@@ -142,37 +142,37 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
142
142
  Tab 은 격자를 **하나로** 세고, 그 안은 화살표로 다닌다.
143
143
  ←→ 하루 · ↑↓ 한 주 · Home/End 그 주의 처음과 끝 · PageUp/Down 한 달 ·
144
144
  Shift 를 얹으면 한 해. min/max 를 넘어가면 그 끝으로 잡아 둔다. */
145
- const [활성일, 활성일설정] = useState(1);
146
- const 격자Ref = useRef(null);
145
+ const [activeDay, setActiveDay] = useState(1);
146
+ const gridRef = useRef(null);
147
147
  /* 옮긴 뒤에 포커스를 준다 — 달을 넘는 이동은 다시 그려진 **뒤**라야 그 칸이
148
148
  생긴다. 효과에서 한 번만 집어 가게 쪽지를 남긴다(매 렌더 훔치지 않도록). */
149
- const 옮길곳 = useRef(null);
149
+ const focusDay = useRef(null);
150
150
  useEffect(() => {
151
151
  if (!open)
152
152
  return;
153
- const 첫자리 = value && value.slice(0, 7) === `${viewY}-${String(viewM + 1).padStart(2, '0')}` ? +value.slice(8)
154
- : 오늘ISO.slice(0, 7) === `${viewY}-${String(viewM + 1).padStart(2, '0')}` ? +오늘ISO.slice(8)
153
+ const firstIndex = value && value.slice(0, 7) === `${viewY}-${String(viewM + 1).padStart(2, '0')}` ? +value.slice(8)
154
+ : todayIso.slice(0, 7) === `${viewY}-${String(viewM + 1).padStart(2, '0')}` ? +todayIso.slice(8)
155
155
  : 1;
156
- 활성일설정(첫자리);
156
+ setActiveDay(firstIndex);
157
157
  // 열 때 한 번만 잡는다 — 그 뒤의 달 이동은 아래 갈무리가 맡는다
158
158
  // eslint-disable-next-line react-hooks/exhaustive-deps
159
159
  }, [open]);
160
160
  useEffect(() => {
161
- if (옮길곳.current == null)
161
+ if (focusDay.current == null)
162
162
  return;
163
- const d = 옮길곳.current;
164
- 옮길곳.current = null;
165
- 격자Ref.current?.querySelector(`[data-day="${d}"]`)?.focus();
163
+ const d = focusDay.current;
164
+ focusDay.current = null;
165
+ gridRef.current?.querySelector(`[data-day="${d}"]`)?.focus();
166
166
  });
167
167
  /* 달이 바뀌면(나침반 단추나 연월 고르개로) 활성일이 그 달 밖일 수 있다.
168
168
  여기서는 **포커스를 안 옮긴다** — 사람은 그 단추 위에 있다. */
169
169
  useEffect(() => {
170
- 활성일설정((d) => Math.min(d, daysInMonth(viewY, viewM)));
170
+ setActiveDay((d) => Math.min(d, daysInMonth(viewY, viewM)));
171
171
  }, [viewY, viewM]);
172
172
  /* 옮길 곳을 날짜로 셈한다 — 날 넘김은 Date 에 맡기고, 달 넘김은 아래에서 말일로 조인다.
173
173
  min/max 밖으로 나가면 그 끝으로 잡는다. */
174
- const 옮기기 = useCallback((다음) => {
175
- let iso = toISO(다음.getFullYear(), 다음.getMonth(), 다음.getDate());
174
+ const moveTo = useCallback((next) => {
175
+ let iso = toISO(next.getFullYear(), next.getMonth(), next.getDate());
176
176
  if (min && iso < min)
177
177
  iso = min;
178
178
  if (max && iso > max)
@@ -180,47 +180,47 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
180
180
  const [y, m, d] = iso.split('-').map(Number);
181
181
  if (y !== viewY || m - 1 !== viewM)
182
182
  setViewYM(y, m - 1);
183
- 활성일설정(d);
184
- 옮길곳.current = d;
183
+ setActiveDay(d);
184
+ focusDay.current = d;
185
185
  }, [min, max, viewY, viewM, setViewYM]);
186
- const 격자키 = useCallback((e) => {
187
- const 기준 = new Date(viewY, viewM, 활성일);
188
- const 하루 = (n) => { 기준.setDate(기준.getDate() + n); 옮기기(기준); };
186
+ const onGridKeyDown = useCallback((e) => {
187
+ const base = new Date(viewY, viewM, activeDay);
188
+ const byDay = (n) => { base.setDate(base.getDate() + n); moveTo(base); };
189
189
  // 달 넘김은 **목표 달의 말일로 조인다** — setMonth 는 말일에서 넘쳐 1/31 → 3/3, 3/31 에서 앞 달로 못 가고, 2028-02-29 → 2029-03-01 이 됐다
190
- const 달 = (n) => {
191
- const 총 = 기준.getFullYear() * 12 + 기준.getMonth() + n;
192
- const y = Math.floor(총 / 12), m = 총 - y * 12;
193
- 옮기기(new Date(y, m, Math.min(기준.getDate(), daysInMonth(y, m))));
190
+ const byMonth = (n) => {
191
+ const total = base.getFullYear() * 12 + base.getMonth() + n;
192
+ const y = Math.floor(total / 12), m = total - y * 12;
193
+ moveTo(new Date(y, m, Math.min(base.getDate(), daysInMonth(y, m))));
194
194
  };
195
195
  switch (e.key) {
196
196
  case 'ArrowLeft':
197
- 하루(-1);
197
+ byDay(-1);
198
198
  break;
199
199
  case 'ArrowRight':
200
- 하루(1);
200
+ byDay(1);
201
201
  break;
202
202
  case 'ArrowUp':
203
- 하루(-7);
203
+ byDay(-7);
204
204
  break;
205
205
  case 'ArrowDown':
206
- 하루(7);
206
+ byDay(7);
207
207
  break;
208
208
  case 'Home':
209
- 하루(-기준.getDay());
209
+ byDay(-base.getDay());
210
210
  break;
211
211
  case 'End':
212
- 하루(6 - 기준.getDay());
212
+ byDay(6 - base.getDay());
213
213
  break;
214
214
  case 'PageUp':
215
- e.shiftKey ? 달(-12) : 달(-1);
215
+ e.shiftKey ? byMonth(-12) : byMonth(-1);
216
216
  break;
217
217
  case 'PageDown':
218
- e.shiftKey ? 달(12) : 달(1);
218
+ e.shiftKey ? byMonth(12) : byMonth(1);
219
219
  break;
220
220
  default: return;
221
221
  }
222
222
  e.preventDefault();
223
- }, [viewY, viewM, 활성일, 옮기기]);
223
+ }, [viewY, viewM, activeDay, moveTo]);
224
224
  const selectDay = useCallback((d) => {
225
225
  if (disabled)
226
226
  return; // 열린 채 disabled 가 되면 남은 달력에서 고른 날이 그대로 나갔다
@@ -239,11 +239,11 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
239
239
  };
240
240
  /* Tab 을 받을 날 — 활성일이 min/max 밖이라 꺼져 있으면 켜진 첫 날로. 꺼진 단추는 Tab 을 못 받아
241
241
  격자에 Tab 자리가 **하나도 없어** 키보드로 달력에 들어갈 수 없었다(값 없음 + min=내일). */
242
- const 탭자리 = (() => {
243
- const 끝 = daysInMonth(viewY, viewM);
244
- if (활성일 >= 1 && 활성일 <= 끝 && !isDisabledDay(활성일))
245
- return 활성일;
246
- for (let d = 1; d <= 끝; d++)
242
+ const tabIndexOf = (() => {
243
+ const lastDay = daysInMonth(viewY, viewM);
244
+ if (activeDay >= 1 && activeDay <= lastDay && !isDisabledDay(activeDay))
245
+ return activeDay;
246
+ for (let d = 1; d <= lastDay; d++)
247
247
  if (!isDisabledDay(d))
248
248
  return d;
249
249
  return -1;
@@ -259,15 +259,15 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
259
259
  const dropdown = open ? createPortal(_jsxs("div", { ref: menuRef, id: panelId, className: "date-picker__dropdown", style: dropdownStyle, "data-tooltip-ignore": true, role: "dialog", "aria-label": t('datePicker.chooseDate', '날짜 선택'), tabIndex: -1, children: [_jsxs("div", { className: "date-picker__nav", children: [_jsx("button", { type: "button", className: "date-picker__nav-btn", onClick: prevMonth, "aria-label": t('calendar.prevMonth', '이전 달'), children: _jsx(ChevronLeft, { size: 16, strokeWidth: 2 }) }), _jsx(Dropdown, { className: "date-picker__nav-dropdown", ariaLabel: t('datePicker.selectYearMonth', '연월 선택'), options: ymOptions, value: ymValue, onChange: (v) => {
260
260
  const [y, m] = v.split('-');
261
261
  setViewYM(+y, +m - 1);
262
- }, block: true, size: 40 }), _jsx("button", { type: "button", className: "date-picker__nav-btn", onClick: nextMonth, "aria-label": t('calendar.nextMonth', '다음 달'), children: _jsx(ChevronRight, { size: 16, strokeWidth: 2 }) })] }), _jsxs("div", { className: "date-picker__grid", ref: 격자Ref, onKeyDown: 격자키, children: [DOW.map(d => (_jsx("div", { className: "date-picker__dow caption-1", children: d }, d))), cells.map((d, i) => {
262
+ }, block: true, size: 40 }), _jsx("button", { type: "button", className: "date-picker__nav-btn", onClick: nextMonth, "aria-label": t('calendar.nextMonth', '다음 달'), children: _jsx(ChevronRight, { size: 16, strokeWidth: 2 }) })] }), _jsxs("div", { className: "date-picker__grid", ref: gridRef, onKeyDown: onGridKeyDown, children: [DOW.map(d => (_jsx("div", { className: "date-picker__dow caption-1", children: d }, d))), cells.map((d, i) => {
263
263
  if (d === null)
264
264
  return _jsx("div", {}, `e${i}`);
265
265
  const iso = toISO(viewY, viewM, d);
266
266
  const isSelected = iso === value;
267
- const isToday = iso === 오늘ISO;
267
+ const isToday = iso === todayIso;
268
268
  const dis = isDisabledDay(d);
269
269
  const col = (i % 7);
270
- return (_jsx("button", { type: "button", disabled: dis, "data-day": d, tabIndex: d === 탭자리 ? 0 : -1, "aria-current": isToday ? 'date' : undefined, onClick: () => !dis && selectDay(d), className: cx('date-picker__day caption-1', isSelected && 'date-picker__day--selected', isToday && !isSelected && 'date-picker__day--today', dis && 'date-picker__day--disabled', col === 0 && 'date-picker__day--sun', col === 6 && 'date-picker__day--sat'), children: d }, d));
270
+ return (_jsx("button", { type: "button", disabled: dis, "data-day": d, tabIndex: d === tabIndexOf ? 0 : -1, "aria-current": isToday ? 'date' : undefined, onClick: () => !dis && selectDay(d), className: cx('date-picker__day caption-1', isSelected && 'date-picker__day--selected', isToday && !isSelected && 'date-picker__day--today', dis && 'date-picker__day--disabled', col === 0 && 'date-picker__day--sun', col === 6 && 'date-picker__day--sat'), children: d }, d));
271
271
  })] })] }), document.body) : null;
272
272
  return (_jsxs("div", { className: cx('date-picker', `date-picker--s${size}`, open && 'date-picker--open', className), children: [_jsxs("button", { ref: triggerRef, type: "button", className: "date-picker__trigger body-2", onClick: () => !disabled && toggle(), disabled: disabled, "aria-haspopup": "true", "aria-expanded": open, children: [_jsx("span", { className: value ? 'date-picker__value' : 'date-picker__placeholder', children: value ? formatDisplay(value) : placeholder }), _jsx(Calendar, { size: iconSize, className: "date-picker__icon", strokeWidth: 1.5 })] }), dropdown] }));
273
273
  }
@@ -40,20 +40,20 @@ export default function Dialog({ open, onClose, ariaLabel, title, children, size
40
40
  useEffect(() => {
41
41
  if (!open)
42
42
  return;
43
- const 돌아갈곳 = document.activeElement;
43
+ const returnTo = document.activeElement;
44
44
  /* 스크롤 잠금은 **공유 카운터**(Popup·TrainingPagePopup 과 같은 것)로 — 전에는 body.style.overflow 를 제 손으로
45
45
  기억했다 되돌려, Dialog 위에 Popup 을 열고 Dialog 를 먼저 닫으면 Popup 이 떠 있는데 스크롤이 풀리고, 이어 Popup 을
46
46
  닫으면 overflow:hidden 으로 영영 굳었다. */
47
- const 풀기 = lockBodyScroll();
48
- const 판 = surfaceRef.current;
49
- rememberFocusReturn(판, 돌아갈곳);
50
- 판?.focus();
47
+ const unlock = lockBodyScroll();
48
+ const surface = surfaceRef.current;
49
+ rememberFocusReturn(surface, returnTo);
50
+ surface?.focus();
51
51
  return () => {
52
- 풀기();
52
+ unlock();
53
53
  // 포커스가 이 창 안이거나 갈 곳을 잃었을 때만 되돌린다 — 이미 다른 창으로 옮겨 갔으면 빼앗지 않는다(Popup 과 같다)
54
- const 지금 = document.activeElement;
55
- if (!지금 || 지금 === document.body || (판 && 판.contains(지금)))
56
- resolveFocusReturn(돌아갈곳)?.focus?.();
54
+ const active = document.activeElement;
55
+ if (!active || active === document.body || (surface && surface.contains(active)))
56
+ resolveFocusReturn(returnTo)?.focus?.();
57
57
  };
58
58
  }, [open]);
59
59
  /* ESC 는 **공유 스택**에 맡긴다 — 예전에는 window 에서 직접 들었다.
@@ -76,7 +76,7 @@ export default function Dialog({ open, onClose, ariaLabel, title, children, size
76
76
  }, [open]);
77
77
  /* 바탕 누르기 — 누른 곳과 뗀 곳이 **둘 다 바탕일 때만** 닫는다(Popup 0.128.0 과 같다). 판 안에서 글자를 긁어
78
78
  고르다 바깥에서 놓으면 click 이 공통 조상(바탕)에 떨어져 창이 닫혔다. */
79
- const 바탕에서누름 = useRef(false);
79
+ const pressedOnBackdrop = useRef(false);
80
80
  /* Tab 을 이 판 안에 가둔다 — `aria-modal="true"` 는 **낭독기에게만** 통한다.
81
81
  눈으로 보며 Tab 을 누르는 사람은 그 표시를 못 보므로, 가둠이 없으면 마지막
82
82
  단추 다음에서 **뒤 화면**으로 빠져나간다. 실제로 그랬다: 놀이터
@@ -86,11 +86,11 @@ export default function Dialog({ open, onClose, ariaLabel, title, children, size
86
86
  useFocusTrap(surfaceRef, open);
87
87
  if (!open || typeof document === 'undefined')
88
88
  return null;
89
- return createPortal(_jsx("div", { className: cx('dialog', className), onPointerDown: closeOnBackdrop ? (e) => { 바탕에서누름.current = e.target === e.currentTarget; } : undefined, onMouseDown: (e) => { if (e.target === e.currentTarget)
89
+ return createPortal(_jsx("div", { className: cx('dialog', className), onPointerDown: closeOnBackdrop ? (e) => { pressedOnBackdrop.current = e.target === e.currentTarget; } : undefined, onMouseDown: (e) => { if (e.target === e.currentTarget)
90
90
  e.preventDefault(); }, onClick: closeOnBackdrop ? (e) => {
91
- const 눌렀나 = 바탕에서누름.current;
92
- 바탕에서누름.current = false;
93
- if (눌렀나 && e.target === e.currentTarget)
91
+ const pressed = pressedOnBackdrop.current;
92
+ pressedOnBackdrop.current = false;
93
+ if (pressed && e.target === e.currentTarget)
94
94
  handleClose();
95
95
  } : undefined, children: _jsxs("div", { ref: surfaceRef, role: "dialog", "aria-modal": "true", "aria-label": title ? undefined : ariaLabel, "aria-labelledby": title ? titleId : undefined, tabIndex: -1, className: cx('dialog__surface', `dialog__surface--${size}`, contentClassName), onClick: (e) => e.stopPropagation(), children: [title ? (_jsx("div", { className: "dialog__header", children: _jsx("h2", { id: titleId, className: "dialog__title h4b", children: title }) })) : null, showClose ? (_jsx("button", { type: "button", className: "dialog__close", "aria-label": t('common.close', '닫기'), onClick: handleClose, children: _jsx(CloseIcon, {}) })) : null, _jsx("div", { className: "dialog__body", children: children })] }) }), document.body);
96
96
  }
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useCallback, useEffect, useState } from "react";
4
4
  import EffectPartyPopper from "./EffectPartyPopper";
5
- const 수 = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined);
5
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined);
6
6
  /**
7
7
  * @public 앱에 한 번 붙여 두는 폭죽 자리.
8
8
  *
@@ -14,17 +14,17 @@ const 수 = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : u
14
14
  * - 이 자리가 앱에 없으면 이벤트는 아무 일도 안 한다.
15
15
  */
16
16
  export default function EffectPartyPopperHost({ trigger = 0, active = false }) {
17
- const [터짐, set터짐] = useState(null);
17
+ const [burst, setBurst] = useState(null);
18
18
  useEffect(() => {
19
19
  if (typeof window === 'undefined')
20
20
  return;
21
- const 받기 = (e) => {
21
+ const onEvent = (e) => {
22
22
  const d = (e.detail ?? {});
23
- set터짐((앞) => ({ n: (앞?.n ?? 0) + 1, count: 수(d.count), strength: 수(d.strength), duration: 수(d.duration) }));
23
+ setBurst((prev) => ({ n: (prev?.n ?? 0) + 1, count: num(d.count), strength: num(d.strength), duration: num(d.duration) }));
24
24
  };
25
- window.addEventListener('party:trigger', 받기);
26
- return () => window.removeEventListener('party:trigger', 받기);
25
+ window.addEventListener('party:trigger', onEvent);
26
+ return () => window.removeEventListener('party:trigger', onEvent);
27
27
  }, []);
28
- const 걷기 = useCallback(() => set터짐(null), []);
29
- return (_jsxs(_Fragment, { children: [_jsx(EffectPartyPopper, { active: active, trigger: trigger }), 터짐 && (_jsx(EffectPartyPopper, { active: true, trigger: 터짐.n, count: 터짐.count, strength: 터짐.strength, duration: 터짐.duration, onComplete: 걷기 }))] }));
28
+ const clear = useCallback(() => setBurst(null), []);
29
+ return (_jsxs(_Fragment, { children: [_jsx(EffectPartyPopper, { active: active, trigger: trigger }), burst && (_jsx(EffectPartyPopper, { active: true, trigger: burst.n, count: burst.count, strength: burst.strength, duration: burst.duration, onComplete: clear }))] }));
30
30
  }
@@ -28,7 +28,7 @@ export default function FilterPanel({ title, titleLevel = 2, headerAction, heade
28
28
  /* 머리 아무 데나 눌러도 접힌다. 다만 **그 안의 조작부를 누른 것은 아니다** —
29
29
  `headerAction` 의 단추(초기화 등)를 누르면 그 일만 하고 판은 그대로 둔다.
30
30
  chevron 단추는 제 `onClick` 으로 접으므로 여기서 한 번 더 접지 않는다(두 번 접히면 제자리다). */
31
- const 머리누름 = (e) => {
31
+ const onHeadClick = (e) => {
32
32
  if (!collapsible)
33
33
  return;
34
34
  const el = e.target;
@@ -44,17 +44,17 @@ export default function FilterPanel({ title, titleLevel = 2, headerAction, heade
44
44
  표 설정 판처럼 값이 아예 `WordTableBlock` 안에 있는 자리는 지을 길조차 없다(말귀 요청 ㉗).
45
45
  `headerSummary` 를 주면 그것이 이긴다 — 글월을 부르는 쪽이 쥐던 길은 그대로 열려 있다.
46
46
  접혀 있을 때만 쓰이므로 펼친 동안은 셈하지 않는다. */
47
- const 기본요약 = useMemo(() => (headerSummary !== undefined || !collapsed ? '' : buildFilterSummary(sections, selected, t('common.all', '전체'))), [headerSummary, collapsed, sections, selected, t]);
48
- const summaryNode = headerSummary ?? (기본요약 ? _jsx("span", { className: "filter-panel__header-summary body-3", children: 기본요약 }) : null);
47
+ const defaultSummary = useMemo(() => (headerSummary !== undefined || !collapsed ? '' : buildFilterSummary(sections, selected, t('common.all', '전체'))), [headerSummary, collapsed, sections, selected, t]);
48
+ const summaryNode = headerSummary ?? (defaultSummary ? _jsx("span", { className: "filter-panel__header-summary body-3", children: defaultSummary }) : null);
49
49
  const showHeader = title || headerAction || headerSummary || collapsible;
50
- const 제목id = useId();
51
- const 제목태그 = titleLevel ? `h${titleLevel}` : 'span';
50
+ const titleId = useId();
51
+ const TitleTag = titleLevel ? `h${titleLevel}` : 'span';
52
52
  return (
53
53
  /* **제목이 이 판의 이름이 된다**(말귀 요청 (57), 2026-10-02). 이름 없는 판은 지역으로
54
54
  안 잡혀, 셋이 나란히 서도 "지금 어느 판 안인지" 를 알 길이 없었다 — (51)·(54) 에서
55
55
  `ChartSection`·`TrainingCardGroup` 에 한 것과 같은 자다.
56
56
  **글자 제목일 때만 건다** — 마디를 가리키므로 글이 아닌 제목이면 이름이 안 선다. */
57
- _jsxs("div", { className: cx('filter-panel', collapsible ? 'filter-panel--collapsible' : '', collapsed ? 'filter-panel--collapsed' : '', className), role: typeof title === 'string' && title ? 'group' : undefined, "aria-labelledby": typeof title === 'string' && title ? 제목id : undefined, children: [showHeader && (_jsxs("div", { className: "filter-panel__header", onClick: collapsible ? 머리누름 : undefined, children: [_jsx("div", { className: "filter-panel__title-row", children: title && _jsx(제목태그, { id: 제목id, className: "filter-panel__title h4", children: title }) }), _jsxs("div", { className: "filter-panel__header-actions", children: [collapsed ? summaryNode : headerAction, collapsible && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-down' : 'chevron-up',
57
+ _jsxs("div", { className: cx('filter-panel', collapsible ? 'filter-panel--collapsible' : '', collapsed ? 'filter-panel--collapsed' : '', className), role: typeof title === 'string' && title ? 'group' : undefined, "aria-labelledby": typeof title === 'string' && title ? titleId : undefined, children: [showHeader && (_jsxs("div", { className: "filter-panel__header", onClick: collapsible ? onHeadClick : undefined, children: [_jsx("div", { className: "filter-panel__title-row", children: title && _jsx(TitleTag, { id: titleId, className: "filter-panel__title h4", children: title }) }), _jsxs("div", { className: "filter-panel__header-actions", children: [collapsed ? summaryNode : headerAction, collapsible && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-down' : 'chevron-up',
58
58
  /* Button은 aria-label을 자기 ariaLabel prop으로 덮어쓴다({...rest} 뒤에
59
59
  aria-label={ariaLabelString}을 다시 쓴다) — kebab으로 넘기면 undefined가
60
60
  되어 이름이 통째로 사라진다. 저장소의 다른 호출부는 모두 ariaLabel을 쓴다.
@@ -15,13 +15,13 @@ export default function FilterSectionInfo({ title, items }) {
15
15
  const { anchorRect, openAt, close: handleClose } = useHoverAnchor();
16
16
  /* 터치 — 톡하면 열렸다 떼자마자 닫혀 설명을 읽을 수 없었다. 톡으로 열고 닫는다(마우스는 그대로).
17
17
  트리거를 품는 자리 — 상자를 만들지 않는(display: contents) 껍데기라 배치는 그대로다. */
18
- const 자리Ref = useRef(null);
19
- const 톡토글 = useTouchToggle(!!anchorRect, (next, el) => { if (next && el)
20
- openAt(자리Ref.current?.querySelector('button') ?? el);
18
+ const anchorRef = useRef(null);
19
+ const touchToggle = useTouchToggle(!!anchorRect, (next, el) => { if (next && el)
20
+ openAt(anchorRef.current?.querySelector('button') ?? el);
21
21
  else
22
- handleClose(); }, 자리Ref);
23
- return (_jsxs("span", { ref: 자리Ref, style: { display: 'contents' }, children: [_jsx(Button, { htmlType: "button", size: 24, type: "dot", icon: "info", variant: "transparent-gray", useBorder: false, selected: !!anchorRect, ariaLabel: t('common.info', '설명'), onPointerEnter: (e) => { if (!isTouchPointer(e))
22
+ handleClose(); }, anchorRef);
23
+ return (_jsxs("span", { ref: anchorRef, style: { display: 'contents' }, children: [_jsx(Button, { htmlType: "button", size: 24, type: "dot", icon: "info", variant: "transparent-gray", useBorder: false, selected: !!anchorRect, ariaLabel: t('common.info', '설명'), onPointerEnter: (e) => { if (!isTouchPointer(e))
24
24
  openAt(e.currentTarget); }, onPointerLeave: (e) => { if (!isTouchPointer(e))
25
- handleClose(); }, onClick: (e) => { 톡토글.touchPress(e.currentTarget); }, onFocus: (e) => { if (!recentlyTouched())
25
+ handleClose(); }, onClick: (e) => { touchToggle.touchPress(e.currentTarget); }, onFocus: (e) => { if (!recentlyTouched())
26
26
  openAt(e.currentTarget); }, onBlur: handleClose }), anchorRect && (_jsx(DescriptionTooltip, { title: title, items: items, showIndex: items.length > 1, anchorRect: anchorRect, onClose: handleClose }))] }));
27
27
  }
@@ -25,5 +25,5 @@ import Button from './Button';
25
25
  * @public
26
26
  */
27
27
  export default function GameIntro({ icon, iconSize = 56, title, titleClass = 'h2', description, tipGroups, start, className, }) {
28
- return (_jsxs("div", { className: cx('game-intro', className), children: [icon != null && (_jsx("span", { className: "game-intro__icon", "aria-hidden": "true", style: { '--game-intro-icon': `${iconSize}px` }, children: icon })), _jsx("h2", { className: cx('game-intro__title', titleClass), children: title }), description != null && _jsx("p", { className: "game-intro__description body-3", children: description }), tipGroups?.map((묶음, i) => (_jsxs("div", { className: "game-intro__tips", children: [묶음.title != null && _jsx("span", { className: "game-intro__tips-title body-3", children: 묶음.title }), 묶음.tips.map((팁, j) => (_jsxs("span", { className: "game-intro__tip body-3", children: [팁.icon != null && _jsx("span", { "aria-hidden": "true", children: 팁.icon }), _jsx("span", { children: 팁.text })] }, j)))] }, i))), start && (_jsx(Button, { htmlType: "button", className: start.className, variant: "main", color: start.color, size: 48, width: "100%", disabled: start.disabled, onClick: start.onClick, children: start.label }))] }));
28
+ return (_jsxs("div", { className: cx('game-intro', className), children: [icon != null && (_jsx("span", { className: "game-intro__icon", "aria-hidden": "true", style: { '--game-intro-icon': `${iconSize}px` }, children: icon })), _jsx("h2", { className: cx('game-intro__title', titleClass), children: title }), description != null && _jsx("p", { className: "game-intro__description body-3", children: description }), tipGroups?.map((group, i) => (_jsxs("div", { className: "game-intro__tips", children: [group.title != null && _jsx("span", { className: "game-intro__tips-title body-3", children: group.title }), group.tips.map((tip, j) => (_jsxs("span", { className: "game-intro__tip body-3", children: [tip.icon != null && _jsx("span", { "aria-hidden": "true", children: tip.icon }), _jsx("span", { children: tip.text })] }, j)))] }, i))), start && (_jsx(Button, { htmlType: "button", className: start.className, variant: "main", color: start.color, size: 48, width: "100%", disabled: start.disabled, onClick: start.onClick, children: start.label }))] }));
29
29
  }
@@ -1,5 +1,5 @@
1
1
  import './HearingDeviceInfo.css';
2
- export type DeviceSide = '좌' | '우';
2
+ export type DeviceSide = 'left' | 'right';
3
3
  export interface DeviceEntry {
4
4
  brand: string;
5
5
  device: string;
@@ -10,10 +10,10 @@ export interface DeviceEntry {
10
10
  wornDay: string;
11
11
  }
12
12
  interface HearingDeviceInfoProps {
13
- devices: {
14
- 좌: DeviceEntry | null;
15
- 우: DeviceEntry | null;
16
- };
13
+ /** 쪽별로 지금 끼고 있는 기기 — 열쇠는 `DeviceSide` **값**(`'left'`·`'right'`)이다.
14
+ * 그 값은 서버의 `side` 열과 그대로 오가는 **자료**라 건드리지 않았고, 공개 자리에 보이는
15
+ * **모양만** 영어 이름으로 돌렸다(`Record`). 값까지 바꾸는 것은 DB 결정이다(사용자 지시 2026-10-13). */
16
+ devices: Record<DeviceSide, DeviceEntry | null>;
17
17
  onAddDevice: (side: DeviceSide) => void;
18
18
  onOpenList: (side: DeviceSide) => void;
19
19
  onRemoveDevice: (side: DeviceSide) => void;
@@ -6,9 +6,9 @@ import { formatWornDate } from '../utils/wornDate';
6
6
  import './HearingDeviceInfo.css';
7
7
  export default function HearingDeviceInfo({ devices, onAddDevice, onOpenList, onRemoveDevice }) {
8
8
  const { t, i18n } = useTranslation(); /* i18n.language — 착용일이 보는 말을 따른다((118)) */
9
- return (_jsxs("div", { className: "hearing-device-info", children: [_jsx("div", { className: "hearing-device-info__title body-3", children: t('customers.device.title', '청각재활기기') }), _jsx("div", { className: "hearing-device-info__row", children: ['좌', '우'].map(side => {
9
+ return (_jsxs("div", { className: "hearing-device-info", children: [_jsx("div", { className: "hearing-device-info__title body-3", children: t('customers.device.title', '청각재활기기') }), _jsx("div", { className: "hearing-device-info__row", children: ['left', 'right'].map(side => {
10
10
  const d = devices[side];
11
- const sideLabel = side === '좌' ? t('device.direction.left', '좌') : t('device.direction.right', '우');
11
+ const sideLabel = side === 'left' ? t('device.direction.left', '좌') : t('device.direction.right', '우');
12
12
  return (_jsxs("div", { className: "hearing-device-info__side", children: [_jsx("span", { className: "hearing-device-info__side-label body-3", children: sideLabel }), d ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "hearing-device-info__device", children: [_jsx("span", { className: "hearing-device-info__device-name body-3", children: d.brand }), _jsxs("span", { className: "hearing-device-info__device-detail", children: [d.device, d.fitType ? ` · ${d.fitType}` : ''] }), d.productName && _jsx("span", { className: "hearing-device-info__device-product", children: d.productName }), formatWornDate(d.wornYear, d.wornMonth, d.wornDay, i18n.language) && _jsx("span", { className: "hearing-device-info__device-date", children: formatWornDate(d.wornYear, d.wornMonth, d.wornDay, i18n.language) })] }), _jsxs("div", { className: "hearing-device-info__actions", children: [_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "plus", onClick: () => onAddDevice(side), showTooltipInfo: t('device.addNew', '새 기기 추가'), ariaLabel: t('device.addNew', '새 기기 추가') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "list", onClick: () => onOpenList(side), showTooltipInfo: t('device.selectFromList', '목록에서 선택'), ariaLabel: t('device.selectFromList', '목록에서 선택') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "danger", onClick: () => onRemoveDevice(side), showTooltipInfo: t('device.removeDevice', '착용 해제'), ariaLabel: t('device.removeDevice', '착용 해제') })] })] })) : (_jsxs("div", { className: "hearing-device-info__actions", children: [_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "plus", onClick: () => onAddDevice(side), showTooltipInfo: t('device.addNew', '새 기기 추가'), ariaLabel: t('device.addNew', '새 기기 추가') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "list", onClick: () => onOpenList(side), showTooltipInfo: t('device.selectFromList', '목록에서 선택'), ariaLabel: t('device.selectFromList', '목록에서 선택') })] }))] }, side));
13
13
  }) })] }));
14
14
  }
@@ -23,10 +23,10 @@ export default function Icon(props) {
23
23
  const createWrapper = (children, extraProps = {}) => (_jsx("span", { className: wrapperClass, style: wrapperStyle, ...extraProps, children: children }));
24
24
  /* 이미지(파일 경로)에도 size 를 한 변 픽셀로 준다 — 전에는 lucide·마스크만 size 를 읽어 PNG·<img> 로 그린 SVG 는
25
25
  원본 크기로 떴다. 줄 때만 박고(object-fit: contain 으로 비율 유지), 안 주면 지금처럼 클래스·원본 크기. */
26
- const 이미지크기 = typeof size === 'number' ? { width: size, height: size, objectFit: 'contain' } : null;
26
+ const imageSize = typeof size === 'number' ? { width: size, height: size, objectFit: 'contain' } : null;
27
27
  const createImage = (src, extraProps = {}) => (
28
28
  // 글자가 아닌 label(요소)은 alt 로 못 쓴다 — String() 하면 "[object Object]" 가 읽혔다
29
- _jsx("img", { src: src, alt: typeof label === 'string' ? label : (typeof label === 'number' ? String(label) : ''), className: defaultImgClass, ...extraProps, style: 이미지크기 ? { ...extraProps.style, ...이미지크기 } : extraProps.style }));
29
+ _jsx("img", { src: src, alt: typeof label === 'string' ? label : (typeof label === 'number' ? String(label) : ''), className: defaultImgClass, ...extraProps, style: imageSize ? { ...extraProps.style, ...imageSize } : extraProps.style }));
30
30
  if (!icon)
31
31
  return null;
32
32
  if (typeof icon !== 'string')
@@ -9,12 +9,12 @@ import './ItemActions.css';
9
9
  * 세 곳에 베껴져 있을 때 실제로 갈라진 것이 이 표의 값들이었다 — 링크 단추의 조건이
10
10
  * 둘은 `&&`, 하나는 늘 세움이었다. 한자리에 있으면 갈라질 자리가 없다.
11
11
  */
12
- const 차례 = [
13
- { 열쇠: 'edit', icon: 'edit-2', 닫고할까: true },
14
- { 열쇠: 'duplicate', icon: 'copy', 닫고할까: false },
15
- { 열쇠: 'publish', icon: 'book-marked', 닫고할까: false },
16
- { 열쇠: 'copyLink', icon: 'link', 닫고할까: false },
17
- { 열쇠: 'remove', icon: 'trash-2', 닫고할까: true },
12
+ const order = [
13
+ { key: 'edit', icon: 'edit-2', closeFirst: true },
14
+ { key: 'duplicate', icon: 'copy', closeFirst: false },
15
+ { key: 'publish', icon: 'book-marked', closeFirst: false },
16
+ { key: 'copyLink', icon: 'link', closeFirst: false },
17
+ { key: 'remove', icon: 'trash-2', closeFirst: true },
18
18
  ];
19
19
  /**
20
20
  * **보고 있는 한 항목에 하는 일들** — 창 바닥 왼쪽에 서는 아이콘 단추 줄.
@@ -67,16 +67,16 @@ const 차례 = [
67
67
  */
68
68
  export default function ItemActions(props) {
69
69
  const { onClose, className } = props;
70
- return (_jsx("div", { className: cx('item-actions', className), children: 차례.map(({ 열쇠, icon, 닫고할까 }) => {
71
- const 받은것 = props[열쇠];
72
- if (!받은것)
70
+ return (_jsx("div", { className: cx('item-actions', className), children: order.map(({ key, icon, closeFirst }) => {
71
+ const given = props[key];
72
+ if (!given)
73
73
  return null;
74
- const 할일 = 열쇠 === 'publish'
75
- ? (받은것.published
76
- ? 받은것.unpublish
77
- : 받은것.publish)
78
- : 받은것;
79
- return (_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: icon, ariaLabel: 할일.label, showTooltipInfo: 할일.label, onClick: () => { if (닫고할까)
80
- onClose(); 할일.onClick(); } }, 열쇠));
74
+ const action = key === 'publish'
75
+ ? (given.published
76
+ ? given.unpublish
77
+ : given.publish)
78
+ : given;
79
+ return (_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: icon, ariaLabel: action.label, showTooltipInfo: action.label, onClick: () => { if (closeFirst)
80
+ onClose(); action.onClick(); } }, key));
81
81
  }) }));
82
82
  }
@@ -34,21 +34,21 @@ export default function LandingHeader({ logo, logoAlt = '', logoHref, links = []
34
34
  if (!open)
35
35
  return;
36
36
  // 서랍 안에 포커스가 있으면 햄버거로 돌려 준다 — 서랍이 hidden 이 되며 포커스가 숨은 링크에 남아 갈 곳을 잃었다
37
- const 풀기 = pushEsc(() => {
38
- const 서랍 = document.getElementById(drawerId);
39
- if (서랍 && 서랍.contains(document.activeElement))
37
+ const popEsc = pushEsc(() => {
38
+ const drawer = document.getElementById(drawerId);
39
+ if (drawer && drawer.contains(document.activeElement))
40
40
  toggleRef.current?.focus();
41
41
  setOpen(false);
42
42
  });
43
43
  const mq = typeof window !== 'undefined' && window.matchMedia ? window.matchMedia('(min-width: 961px)') : null;
44
- const 넓어짐 = (e) => { if (e.matches)
44
+ const onWiden = (e) => { if (e.matches)
45
45
  setOpen(false); };
46
46
  if (mq) {
47
47
  if (mq.matches)
48
48
  setOpen(false);
49
- mq.addEventListener?.('change', 넓어짐);
49
+ mq.addEventListener?.('change', onWiden);
50
50
  }
51
- return () => { 풀기(); mq?.removeEventListener?.('change', 넓어짐); };
51
+ return () => { popEsc(); mq?.removeEventListener?.('change', onWiden); };
52
52
  }, [open, drawerId]);
53
53
  const innerClass = containerClassName
54
54
  ? `${containerClassName} landing-header__inner`