@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
@@ -8,27 +8,27 @@ import { FOCUSABLE_SELECTOR } from './useFocusTrap';
8
8
  * 미리보기와 함께 사라진다. 확인 창을 닫으면 떼어진 단추에 focus() 해 아무 일도 없이 body 에 떨어졌다.
9
9
  * 각 창의 판(panel) → 그 창이 돌려줄 곳을 적어 두고, 돌려줄 곳이 떼어졌으면 그것을 품었던 창의 돌려줄 곳을 따라 올라간다.
10
10
  */
11
- const 돌아갈곳표 = new WeakMap();
11
+ const returnTargets = new WeakMap();
12
12
  /** 창 판과, 그 창이 열릴 때 포커스였던 요소를 적는다 */
13
13
  export function rememberFocusReturn(panel, previouslyFocused) {
14
14
  if (panel)
15
- 돌아갈곳표.set(panel, previouslyFocused);
15
+ returnTargets.set(panel, previouslyFocused);
16
16
  }
17
17
  /** 실제로 돌려줄 곳 — 문서에 붙어 있으면 그대로, 떼어졌으면 그것을 품었던 창이 적어 둔 곳을 따라간다 */
18
18
  export function resolveFocusReturn(previouslyFocused) {
19
- let 대상 = previouslyFocused;
20
- for (let 번 = 0; 대상 && !대상.isConnected && 번 < 10; 번++) {
21
- let 창 = 대상;
22
- while (창 && !돌아갈곳표.has(창))
23
- 창 = 창.parentElement;
24
- if (!창)
25
- return 대상;
26
- 대상 = 돌아갈곳표.get(창) ?? null;
19
+ let target = previouslyFocused;
20
+ for (let hop = 0; target && !target.isConnected && hop < 10; hop++) {
21
+ let node = target;
22
+ while (node && !returnTargets.has(node))
23
+ node = node.parentElement;
24
+ if (!node)
25
+ return target;
26
+ target = returnTargets.get(node) ?? null;
27
27
  }
28
- return 대상;
28
+ return target;
29
29
  }
30
30
  /* 포커스를 받을 수 있는가 — 붙어 있고, 막히지 않았고, inert·aria-hidden·hidden 아래가 아니다(레이아웃은 재지 않는다: 시험 환경에 없다) */
31
- function 받을수있나(el) {
31
+ function canFocus(el) {
32
32
  if (!el || !el.isConnected || el === document.body)
33
33
  return false;
34
34
  if (typeof el.focus !== 'function')
@@ -53,24 +53,24 @@ function 받을수있나(el) {
53
53
  * 옮긴 요소를 돌려준다(못 옮기면 null).
54
54
  */
55
55
  export function moveFocusOutside(container, opener, anchor) {
56
- const 기준 = container?.isConnected ? container : anchor?.isConnected ? anchor : null;
57
- if (받을수있나(opener) && !(container && container.contains(opener))) {
56
+ const anchorNode = container?.isConnected ? container : anchor?.isConnected ? anchor : null;
57
+ if (canFocus(opener) && !(container && container.contains(opener))) {
58
58
  opener.focus();
59
59
  return opener;
60
60
  }
61
- if (!기준)
61
+ if (!anchorNode)
62
62
  return null;
63
- const 모두 = Array.from(document.querySelectorAll(FOCUSABLE_SELECTOR))
64
- .filter((el) => !(container && container.contains(el)) && 받을수있나(el));
65
- let 앞 = null;
66
- let 뒤 = null;
67
- for (const el of 모두) {
68
- if (기준.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING)
69
- 앞 = el;
70
- else if (!뒤)
71
- 뒤 = el;
63
+ const allFocusable = Array.from(document.querySelectorAll(FOCUSABLE_SELECTOR))
64
+ .filter((el) => !(container && container.contains(el)) && canFocus(el));
65
+ let before = null;
66
+ let after = null;
67
+ for (const el of allFocusable) {
68
+ if (anchorNode.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING)
69
+ before = el;
70
+ else if (!after)
71
+ after = el;
72
72
  }
73
- const 대상 = 앞 ?? 뒤;
74
- 대상?.focus();
75
- return 대상;
73
+ const target = before ?? after;
74
+ target?.focus();
75
+ return target;
76
76
  }
@@ -1,2 +1,2 @@
1
1
  /** 마지막 입력이 키보드였으면 true(모르면 false) */
2
- export declare function 키보드로눌렀나(): boolean;
2
+ export declare function wasKeyboardPress(): boolean;
@@ -6,26 +6,26 @@
6
6
  * 초점을 옮겨 그 단추의 초점 툴팁이 떴다(듣기 → '재생 / 멈춤', 문장 미리보기 → '글자 크기 감소'). Safari 는 누른 단추에 초점을
7
7
  * 안 줘 body 에 남는다. 문서 전체에서 키 누름·포인터 누름을 들어 마지막 것을 기억한다(캡처 단계, 한 번만 붙인다).
8
8
  */
9
- let 마지막 = null;
10
- let 붙음 = false;
11
- function 붙이기() {
12
- if (붙음 || typeof document === 'undefined')
9
+ let last = null;
10
+ let attached = false;
11
+ function attach() {
12
+ if (attached || typeof document === 'undefined')
13
13
  return;
14
- 붙음 = true;
14
+ attached = true;
15
15
  document.addEventListener('keydown', (e) => {
16
16
  if (e.metaKey || e.ctrlKey || e.altKey)
17
17
  return;
18
- 마지막 = 'keyboard';
18
+ last = 'keyboard';
19
19
  }, true);
20
- const 포인터 = () => { 마지막 = 'pointer'; };
21
- document.addEventListener('pointerdown', 포인터, true);
22
- document.addEventListener('mousedown', 포인터, true);
23
- document.addEventListener('touchstart', 포인터, { capture: true, passive: true });
20
+ const onPointer = () => { last = 'pointer'; };
21
+ document.addEventListener('pointerdown', onPointer, true);
22
+ document.addEventListener('mousedown', onPointer, true);
23
+ document.addEventListener('touchstart', onPointer, { capture: true, passive: true });
24
24
  }
25
25
  // 불러오는 즉시 듣는다 — 첫 누름부터 알아야 한다
26
- 붙이기();
26
+ attach();
27
27
  /** 마지막 입력이 키보드였으면 true(모르면 false) */
28
- export function 키보드로눌렀나() {
29
- 붙이기();
30
- return 마지막 === 'keyboard';
28
+ export function wasKeyboardPress() {
29
+ attach();
30
+ return last === 'keyboard';
31
31
  }
@@ -12,25 +12,25 @@ import { useLatestRef } from './useLatestRef';
12
12
  * 마우스 동작은 그대로 두고, pointerType 이 touch 일 때만: 들어오고 나감은 무시 · 뗄 때 뒤집기 · 방금 톡으로 온 focus 는 무시 ·
13
13
  * 바깥을 톡하면 닫기.
14
14
  */
15
- let 마지막터치 = 0;
16
- let 붙음 = false;
17
- function 붙이기() {
18
- if (붙음 || typeof document === 'undefined')
15
+ let lastTouchAt = 0;
16
+ let attached = false;
17
+ function attach() {
18
+ if (attached || typeof document === 'undefined')
19
19
  return;
20
- 붙음 = true;
20
+ attached = true;
21
21
  document.addEventListener('pointerdown', (e) => { if (e.pointerType === 'touch')
22
- 마지막터치 = Date.now(); }, true);
23
- document.addEventListener('touchstart', () => { 마지막터치 = Date.now(); }, { capture: true, passive: true });
22
+ lastTouchAt = Date.now(); }, true);
23
+ document.addEventListener('touchstart', () => { lastTouchAt = Date.now(); }, { capture: true, passive: true });
24
24
  }
25
- 붙이기();
25
+ attach();
26
26
  /** 터치 포인터 이벤트인가 */
27
27
  export function isTouchPointer(e) {
28
28
  return !!e && e.pointerType === 'touch';
29
29
  }
30
30
  /** 방금(1초 안) 화면을 톡했나 — 톡 뒤 따라오는 focus·호환 마우스 이벤트를 거를 때 */
31
31
  export function recentlyTouched(ms = 1000) {
32
- 붙이기();
33
- return Date.now() - 마지막터치 < ms;
32
+ attach();
33
+ return Date.now() - lastTouchAt < ms;
34
34
  }
35
35
  /**
36
36
  * 톡으로 뒤집는 손잡이. `open` 은 지금 열림, `setOpen(next, el)` 은 열기(el 은 앵커)·닫기.
@@ -45,39 +45,39 @@ export function useTouchToggle(open, setOpen, rootRef, { enabled = true, fallbac
45
45
  const openRef = useLatestRef(open);
46
46
  const setOpenRef = useLatestRef(setOpen);
47
47
  /** 이 트리거 위 톡 누름 때의 열림(null 이면 톡 누름이 아니었다) */
48
- const 누를때열림 = useRef(null);
49
- const 뒤집은시각 = useRef(0);
48
+ const openOnPress = useRef(null);
49
+ const toggledAt = useRef(0);
50
50
  useEffect(() => {
51
51
  if (!enabled || typeof document === 'undefined')
52
52
  return;
53
- const 누름 = (ev) => {
53
+ const onPress = (ev) => {
54
54
  const pe = ev;
55
55
  const t = ev.target;
56
56
  const root = rootRef.current;
57
- const 안 = !!(t && root && root.contains(t));
57
+ const inside = !!(t && root && root.contains(t));
58
58
  if (pe.pointerType !== 'touch') {
59
- if (안)
60
- 누를때열림.current = null;
59
+ if (inside)
60
+ openOnPress.current = null;
61
61
  return;
62
62
  }
63
- if (안) {
64
- 누를때열림.current = openRef.current;
63
+ if (inside) {
64
+ openOnPress.current = openRef.current;
65
65
  return;
66
66
  }
67
- 누를때열림.current = null;
67
+ openOnPress.current = null;
68
68
  if (openRef.current && !(t && t.closest?.('.tooltip-content')))
69
69
  setOpenRef.current(false, root);
70
70
  };
71
- document.addEventListener('pointerdown', 누름, true);
72
- return () => document.removeEventListener('pointerdown', 누름, true);
71
+ document.addEventListener('pointerdown', onPress, true);
72
+ return () => document.removeEventListener('pointerdown', onPress, true);
73
73
  }, [enabled, rootRef, openRef, setOpenRef]);
74
74
  const touchPress = useCallback((el) => {
75
- const 전 = 누를때열림.current;
76
- if (전 === null)
75
+ const before = openOnPress.current;
76
+ if (before === null)
77
77
  return false;
78
- 누를때열림.current = null;
79
- 뒤집은시각.current = Date.now();
80
- setOpenRef.current(!전, el ?? rootRef.current);
78
+ openOnPress.current = null;
79
+ toggledAt.current = Date.now();
80
+ setOpenRef.current(!before, el ?? rootRef.current);
81
81
  return true;
82
82
  }, [rootRef, setOpenRef]);
83
83
  /* 막힌 native 단추 — 포인터 이벤트가 안 오면 감싼 요소에서 톡(touchstart→touchend)을 받아 자리로 가린다 */
@@ -88,27 +88,27 @@ export function useTouchToggle(open, setOpen, rootRef, { enabled = true, fallbac
88
88
  const parent = root?.parentElement;
89
89
  if (!root || !parent)
90
90
  return;
91
- let 시작때열림 = null;
92
- const 안에있나 = (e) => {
91
+ let openAtStart = null;
92
+ const isInside = (e) => {
93
93
  const p = e.changedTouches?.[0];
94
94
  if (!p)
95
95
  return false;
96
96
  const r = root.getBoundingClientRect();
97
97
  return p.clientX >= r.left && p.clientX <= r.right && p.clientY >= r.top && p.clientY <= r.bottom;
98
98
  };
99
- const 시작 = (e) => { 시작때열림 = 안에있나(e) ? openRef.current : null; };
100
- const 끝 = (e) => {
101
- const 전 = 시작때열림;
102
- 시작때열림 = null;
103
- if (전 === null || Date.now() - 뒤집은시각.current < 700 || !안에있나(e))
99
+ const onStart = (e) => { openAtStart = isInside(e) ? openRef.current : null; };
100
+ const onEnd = (e) => {
101
+ const before = openAtStart;
102
+ openAtStart = null;
103
+ if (before === null || Date.now() - toggledAt.current < 700 || !isInside(e))
104
104
  return;
105
- 누를때열림.current = null;
106
- 뒤집은시각.current = Date.now();
107
- setOpenRef.current(!전, root);
105
+ openOnPress.current = null;
106
+ toggledAt.current = Date.now();
107
+ setOpenRef.current(!before, root);
108
108
  };
109
- parent.addEventListener('touchstart', 시작, { passive: true });
110
- parent.addEventListener('touchend', 끝);
111
- return () => { parent.removeEventListener('touchstart', 시작); parent.removeEventListener('touchend', 끝); };
109
+ parent.addEventListener('touchstart', onStart, { passive: true });
110
+ parent.addEventListener('touchend', onEnd);
111
+ return () => { parent.removeEventListener('touchstart', onStart); parent.removeEventListener('touchend', onEnd); };
112
112
  }, [enabled, fallbackOnParent, rootRef, openRef, setOpenRef]);
113
113
  return { touchPress };
114
114
  }
@@ -10,15 +10,15 @@ import { pushEsc } from './escStack';
10
10
  * 트리거 조상 가운데 가장 높은 z-index 바로 위(없으면 `--z-menu`)로 둔다. 그 층들은 모두 알림·토스트 아래다.
11
11
  */
12
12
  export function anchoredMenuZIndex(trigger) {
13
- let 가장높은 = 0;
13
+ let tallest = 0;
14
14
  if (trigger && typeof window !== 'undefined' && typeof window.getComputedStyle === 'function') {
15
15
  for (let el = trigger; el && el !== document.body && el !== document.documentElement; el = el.parentElement) {
16
16
  const z = parseInt(window.getComputedStyle(el).zIndex, 10);
17
- if (!Number.isNaN(z) && z > 가장높은)
18
- 가장높은 = z;
17
+ if (!Number.isNaN(z) && z > tallest)
18
+ tallest = z;
19
19
  }
20
20
  }
21
- return 가장높은 > 0 ? `max(var(--z-menu, 1500), ${가장높은 + 1})` : 'var(--z-menu, 1500)';
21
+ return tallest > 0 ? `max(var(--z-menu, 1500), ${tallest + 1})` : 'var(--z-menu, 1500)';
22
22
  }
23
23
  /**
24
24
  * 트리거 요소에 fixed 포지션으로 앵커링되는 메뉴/드롭다운/팝오버의 공용 로직.
@@ -53,28 +53,28 @@ export function useAnchoredMenu({ computeStyle, onScroll = 'close', onResize = '
53
53
  (약 40px)이 오르내린다. 높이가 바뀔 때마다 다시 센다.
54
54
  **높이가 달라졌을 때만** 다시 센다 — 안 그러면 셈이 자리를 바꾸고 자리가 다시
55
55
  셈을 불러 끝없이 돈다. */
56
- let 잰높이 = menuRef.current?.offsetHeight ?? 0;
57
- if (잰높이 > 0)
56
+ let measuredHeight = menuRef.current?.offsetHeight ?? 0;
57
+ if (measuredHeight > 0)
58
58
  recalc();
59
59
  /* **오가는 것을 끊는다.** 높이가 달라질 때만 세는 것으로는 두 꼴을 번갈아 오가는 것을
60
60
  못 막는다(A 로 자리를 잡으면 높이가 B 가 되고, B 로 잡으면 다시 A 가 되는 꼴).
61
61
  한 번 여는 동안 몇 번까지만 센다 — 제대로 된 다시 재기는 한두 번이면 끝난다. */
62
- let 센횟수 = 0;
63
- let 판관찰 = null;
62
+ let measureCount = 0;
63
+ let panelObserver = null;
64
64
  if (typeof ResizeObserver !== 'undefined' && menuRef.current) {
65
- 판관찰 = new ResizeObserver(() => {
65
+ panelObserver = new ResizeObserver(() => {
66
66
  const h = menuRef.current?.offsetHeight ?? 0;
67
- if (h === 잰높이)
67
+ if (h === measuredHeight)
68
68
  return;
69
- 잰높이 = h;
70
- if (++센횟수 > 4) {
71
- 판관찰?.disconnect();
72
- 판관찰 = null;
69
+ measuredHeight = h;
70
+ if (++measureCount > 4) {
71
+ panelObserver?.disconnect();
72
+ panelObserver = null;
73
73
  return;
74
74
  }
75
75
  recalc();
76
76
  });
77
- 판관찰.observe(menuRef.current);
77
+ panelObserver.observe(menuRef.current);
78
78
  }
79
79
  const handleOutsideClick = (e) => {
80
80
  const target = e.target;
@@ -94,21 +94,21 @@ export function useAnchoredMenu({ computeStyle, onScroll = 'close', onResize = '
94
94
  capture 로** 받는다 — `DatePicker` 가 `onScroll: 'reposition'` 을 주므로 달력이 열린 동안 굴리면
95
95
  틀마다 여러 번 돌았다. **닫는 길('close')은 안 미룬다** — 미루면 앵커에서 떨어진 판이 한 틀 더
96
96
  허공에 남는다(Tooltip 과 같은 가름). 미루는 것은 *자리* 뿐이다. */
97
- let 자리예약 = null;
98
- const 자리다시 = () => {
97
+ let rafId = null;
98
+ const reposition = () => {
99
99
  if (typeof requestAnimationFrame === 'undefined') {
100
100
  recalc();
101
101
  return;
102
102
  }
103
- if (자리예약 !== null)
103
+ if (rafId !== null)
104
104
  return;
105
- 자리예약 = requestAnimationFrame(() => { 자리예약 = null; recalc(); });
105
+ rafId = requestAnimationFrame(() => { rafId = null; recalc(); });
106
106
  };
107
107
  const handleScroll = (e) => {
108
108
  if (menuRef.current?.contains(e.target))
109
109
  return;
110
110
  if (onScroll === 'reposition')
111
- 자리다시();
111
+ reposition();
112
112
  else if (onScroll === 'close')
113
113
  setOpen(false);
114
114
  };
@@ -116,17 +116,17 @@ export function useAnchoredMenu({ computeStyle, onScroll = 'close', onResize = '
116
116
  window.addEventListener('scroll', handleScroll, true);
117
117
  const handleResize = () => {
118
118
  if (onResize === 'reposition')
119
- 자리다시();
119
+ reposition();
120
120
  else if (onResize === 'close')
121
121
  setOpen(false);
122
122
  };
123
123
  /* 회전(orientationchange)도 함께 — iOS Safari 는 회전 직후 resize 가 늦거나 옛 크기로 오는 일이 있어 회전 뒤 한 번 더 잰다 */
124
- let 회전타이머 = null;
124
+ let rotateTimer = null;
125
125
  const handleOrientation = () => {
126
126
  handleResize();
127
- if (회전타이머)
128
- clearTimeout(회전타이머);
129
- 회전타이머 = setTimeout(() => { 회전타이머 = null; if (onResize === 'reposition')
127
+ if (rotateTimer)
128
+ clearTimeout(rotateTimer);
129
+ rotateTimer = setTimeout(() => { rotateTimer = null; if (onResize === 'reposition')
130
130
  recalc(); }, 250);
131
131
  };
132
132
  if (onResize !== 'ignore') {
@@ -134,9 +134,9 @@ export function useAnchoredMenu({ computeStyle, onScroll = 'close', onResize = '
134
134
  window.addEventListener('orientationchange', handleOrientation);
135
135
  }
136
136
  return () => {
137
- if (자리예약 !== null)
138
- cancelAnimationFrame(자리예약);
139
- 판관찰?.disconnect();
137
+ if (rafId !== null)
138
+ cancelAnimationFrame(rafId);
139
+ panelObserver?.disconnect();
140
140
  document.removeEventListener('mousedown', handleOutsideClick);
141
141
  document.removeEventListener('pointerdown', handleOutsideClick);
142
142
  if (onScroll !== 'ignore')
@@ -145,8 +145,8 @@ export function useAnchoredMenu({ computeStyle, onScroll = 'close', onResize = '
145
145
  window.removeEventListener('resize', handleResize);
146
146
  window.removeEventListener('orientationchange', handleOrientation);
147
147
  }
148
- if (회전타이머)
149
- clearTimeout(회전타이머);
148
+ if (rotateTimer)
149
+ clearTimeout(rotateTimer);
150
150
  };
151
151
  }, [open, recalc, onScroll, onResize, ignoreOutsideClickSelector]);
152
152
  /* ESC 로 닫기 — **공유 ESC 스택**에 올린다(Popup 과 같은 스택). 전에는 document 에서 따로 들어,
@@ -34,25 +34,25 @@ export function useDailyTrainingSelection(userId) {
34
34
  const [hasData, setHasData] = useState(false);
35
35
  /* 오늘(KST) — 자정을 넘기면 바뀌어 아래 선택을 **새 날짜 시드로** 다시 계산하게 한다. 전에는 userId 만 봐서 화면을 열어 둔 채
36
36
  자정을 넘기면 어제 시드의 세트가 '오늘' 로 보이는데 완료 기록에는 새 날짜가 찍혔다. */
37
- const [오늘, set오늘] = useState(() => getTodayKst());
37
+ const [today, setToday] = useState(() => getTodayKst());
38
38
  useEffect(() => {
39
- const 다음자정까지 = () => {
40
- const 지금 = Date.now();
41
- const kst = new Date(지금 + 9 * 3600_000);
42
- const 자정 = Date.UTC(kst.getUTCFullYear(), kst.getUTCMonth(), kst.getUTCDate() + 1) - 9 * 3600_000;
43
- return Math.max(1000, 자정 - 지금 + 1000);
39
+ const msUntilMidnight = () => {
40
+ const now = Date.now();
41
+ const kst = new Date(now + 9 * 3600_000);
42
+ const midnight = Date.UTC(kst.getUTCFullYear(), kst.getUTCMonth(), kst.getUTCDate() + 1) - 9 * 3600_000;
43
+ return Math.max(1000, midnight - now + 1000);
44
44
  };
45
- let t = setTimeout(function 넘김() { set오늘(getTodayKst()); t = setTimeout(넘김, 다음자정까지()); }, 다음자정까지());
45
+ let t = setTimeout(function tick() { setToday(getTodayKst()); t = setTimeout(tick, msUntilMidnight()); }, msUntilMidnight());
46
46
  return () => clearTimeout(t);
47
47
  }, []);
48
48
  useEffect(() => {
49
49
  let cancelled = false;
50
- let 떠남 = false;
51
- let 적용됨 = false;
50
+ let left = false;
51
+ let applied = false;
52
52
  let timer;
53
53
  let attempts = 0;
54
- let 받는중 = null;
55
- let 다음받기 = 0; // 이 attempts 에 이르면 자료 받기를 다시 청한다(2초 = 8틱마다)
54
+ let fetching = null;
55
+ let nextFetchAt = 0; // 이 attempts 에 이르면 자료 받기를 다시 청한다(2초 = 8틱마다)
56
56
  const MAX_ATTEMPTS = 40; // 250ms * 40 = 10초 후 폴링 중단(풀이 영영 비는 경우 무한루프 방지)
57
57
  const stop = () => { if (timer !== undefined) {
58
58
  clearInterval(timer);
@@ -66,9 +66,9 @@ export function useDailyTrainingSelection(userId) {
66
66
  }
67
67
  catch { }
68
68
  const apply = (sel) => {
69
- if (떠남)
69
+ if (left)
70
70
  return;
71
- 적용됨 = true;
71
+ applied = true;
72
72
  todayRef.current = sel.today;
73
73
  yesterdayRef.current = sel.yesterday;
74
74
  setLoaded(true);
@@ -92,19 +92,19 @@ export function useDailyTrainingSelection(userId) {
92
92
  배포(형식 오류)에서 10초에 850KB 자료를 40번 받았다. 그 사이 폴링은 선택만 다시 계산한다. */
93
93
  /* **받는 중이면 이 틱은 건너뛴다** — 받기는 3초까지 기다리는데 250ms 틱이 그 사이 attempts 가 안 오른 채 들어와
94
94
  ensureTrainingDataLoaded 를 매번 새로 불러, 끝나지 않은 받기마다 3초 타이머가 열두 개씩 쌓였다. 진행 중인 약속을 하나만 둔다. */
95
- if (받는중) {
95
+ if (fetching) {
96
96
  if (++attempts >= MAX_ATTEMPTS)
97
- 포기();
97
+ giveUp();
98
98
  return;
99
99
  } // 건너뛴 틱도 10초 시한에 센다
100
- if (attempts >= 다음받기) {
101
- 다음받기 = attempts + 8;
102
- 받는중 = ensureTrainingDataLoaded(3000).catch(() => false);
100
+ if (attempts >= nextFetchAt) {
101
+ nextFetchAt = attempts + 8;
102
+ fetching = ensureTrainingDataLoaded(3000).catch(() => false);
103
103
  try {
104
- await 받는중;
104
+ await fetching;
105
105
  }
106
106
  finally {
107
- 받는중 = null;
107
+ fetching = null;
108
108
  }
109
109
  if (cancelled)
110
110
  return;
@@ -118,35 +118,35 @@ export function useDailyTrainingSelection(userId) {
118
118
  // 포기한다. hasData 를 반드시 false 로 되돌린다 — userId 가 바뀌어 다시 돌 때,
119
119
  // 앞 사람 것이 성공했다면 true 로 남아 있어 "자료가 있다" 고 거짓말하게 된다.
120
120
  if (++attempts >= MAX_ATTEMPTS)
121
- 포기();
121
+ giveUp();
122
122
  };
123
- function 포기() { setLoaded(true); setHasData(false); cancelled = true; stop(); }
123
+ function giveUp() { setLoaded(true); setHasData(false); cancelled = true; stop(); }
124
124
  /* 포기한 뒤(10초)라도 자료가 도착하면 다시 계산한다 — 느린 회선에서 13초 걸려 받은 회원은 새로고침 전까지 "자료를 못 찾음" 에 남았다 */
125
- const 자료도착 = () => {
126
- if (떠남 || 적용됨)
125
+ const onData = () => {
126
+ if (left || applied)
127
127
  return;
128
128
  const sel = resolveSelection(forceRefresh, undefined, userId);
129
129
  if (hasItems(sel.today))
130
130
  apply(sel);
131
131
  };
132
132
  try {
133
- window.addEventListener('speechear.data.updated', 자료도착);
133
+ window.addEventListener('speechear.data.updated', onData);
134
134
  }
135
135
  catch { }
136
136
  tryFill();
137
137
  timer = setInterval(tryFill, 250);
138
138
  return () => {
139
139
  cancelled = true;
140
- 떠남 = true;
140
+ left = true;
141
141
  stop();
142
142
  try {
143
- window.removeEventListener('speechear.data.updated', 자료도착);
143
+ window.removeEventListener('speechear.data.updated', onData);
144
144
  }
145
145
  catch { }
146
146
  };
147
147
  // 오늘 은 날짜가 바뀌었다는 신호 — resolveSelection 이 안에서 오늘 날짜를 읽는다
148
148
  // eslint-disable-next-line react-hooks/exhaustive-deps
149
- }, [userId, 오늘]);
149
+ }, [userId, today]);
150
150
  const getTrainingPerType = useCallback((typeKey) => {
151
151
  const src = showYesterday ? yesterdayRef.current : todayRef.current;
152
152
  const fallback = showYesterday ? todayRef.current : yesterdayRef.current;
@@ -1,5 +1,5 @@
1
1
  import type { DeviceForm } from '../types/device';
2
- type Side = '좌' | '우';
2
+ type Side = 'left' | 'right';
3
3
  /**
4
4
  * 보청기 기기 등록 / 이력 관리 상태를 캡슐화한 훅.
5
5
  */
@@ -11,22 +11,10 @@ export declare function useDeviceManagement(): {
11
11
  setShowDevice: import("react").Dispatch<import("react").SetStateAction<boolean>>;
12
12
  deviceDirection: Side;
13
13
  setDeviceDirection: import("react").Dispatch<import("react").SetStateAction<Side>>;
14
- devices: {
15
- 좌: DeviceForm | null;
16
- 우: DeviceForm | null;
17
- };
18
- setDevices: import("react").Dispatch<import("react").SetStateAction<{
19
- 좌: DeviceForm | null;
20
- 우: DeviceForm | null;
21
- }>>;
22
- deviceHistory: {
23
- 좌: DeviceForm[];
24
- 우: DeviceForm[];
25
- };
26
- setDeviceHistory: import("react").Dispatch<import("react").SetStateAction<{
27
- 좌: DeviceForm[];
28
- 우: DeviceForm[];
29
- }>>;
14
+ devices: Record<Side, DeviceForm | null>;
15
+ setDevices: import("react").Dispatch<import("react").SetStateAction<Record<Side, DeviceForm | null>>>;
16
+ deviceHistory: Record<Side, DeviceForm[]>;
17
+ setDeviceHistory: import("react").Dispatch<import("react").SetStateAction<Record<Side, DeviceForm[]>>>;
30
18
  showDeviceList: boolean;
31
19
  setShowDeviceList: import("react").Dispatch<import("react").SetStateAction<boolean>>;
32
20
  listForSide: Side;
@@ -1,7 +1,17 @@
1
1
  'use client';
2
2
  import { useCallback, useState } from 'react';
3
- /** form.direction 이 '좌'·'우' 가 아니면 null — 캐스팅만 하면 devices['엉뚱한값'] 같은 열쇠가 생긴다 */
4
- const toSide = (d) => (d === '좌' || d === '우' ? d : null);
3
+ /**
4
+ * `form.direction` 이 `'left'`·`'right'` 가 아니면 null — 캐스팅만 하면 `devices['엉뚱한값']` 같은 열쇠가 생긴다.
5
+ *
6
+ * **옛 값(`'좌'`·`'우'`)은 받지 않는다.** 2026-10-13 사장님 결정으로 값을 영어로 옮겼고,
7
+ * 너그럽게 받는 몫은 **소비자가 진다** — 말귀가 DB 와 오가는 길목 한 자리에 세워 두었다
8
+ * (`toDbDirection`/`fromDbDirection`). 여기서 함께 받으면 **두 자리가 같은 일을 하고**,
9
+ * DB 가 다 옮겨진 뒤에도 어느 쪽이 걷혔는지 아무도 모르게 된다.
10
+ *
11
+ * 걸리면 **조용히 `null`** 이다 — 터지지도 찍지도 않는다. 옛 값이 그대로 들어오면
12
+ * 기기가 안 골라지는 꼴로 보이니, 소비자는 **코어에 넘기기 전에** 옮겨 둔다.
13
+ */
14
+ const toSide = (d) => (d === 'left' || d === 'right' ? d : null);
5
15
  /**
6
16
  * 보청기 기기 등록 / 이력 관리 상태를 캡슐화한 훅.
7
17
  */
@@ -10,15 +20,17 @@ const toSide = (d) => (d === '좌' || d === '우' ? d : null);
10
20
  */
11
21
  export function useDeviceManagement() {
12
22
  const [showDevice, setShowDevice] = useState(false);
13
- const [deviceDirection, setDeviceDirection] = useState('좌');
14
- const [devices, setDevices] = useState({ 좌: null, 우: null });
15
- const [deviceHistory, setDeviceHistory] = useState({ 좌: [], 우: [] });
23
+ const [deviceDirection, setDeviceDirection] = useState('left');
24
+ /* 열쇠는 `Side`(= `DeviceSide`) **값**(`'left'`·`'right'`) — 서버의 `side` 열에 오가는 자료다.
25
+ 이름도 값도 영어다 — 값은 2026-10-13 사장님 결정으로 옮겼다(`'좌'`·`'우'` → `'left'`·`'right'`). */
26
+ const [devices, setDevices] = useState({ left: null, right: null });
27
+ const [deviceHistory, setDeviceHistory] = useState({ left: [], right: [] });
16
28
  const [showDeviceList, setShowDeviceList] = useState(false);
17
- const [listForSide, setListForSide] = useState('좌');
29
+ const [listForSide, setListForSide] = useState('left');
18
30
  // form.direction은 각 팝업(DeviceRegistrationPopup의 방향 드롭다운, DeviceListPopup의
19
- // 좌/우 탭)에서 사용자가 실제로 고른 값이다 — deviceDirection/listForSide는 팝업을 열 때의
31
+ // left/right 탭)에서 사용자가 실제로 고른 값이다 — deviceDirection/listForSide는 팝업을 열 때의
20
32
  // 초기값일 뿐이라 팝업 내부에서 방향을 바꾸면 더 이상 실제 선택과 일치하지 않는다.
21
- // 방향이 비었거나 '좌'·'우' 밖이면 **아무것도 바꾸지 않는다** — 예전에는 검사 없이 캐스팅해
33
+ // 방향이 비었거나 'left'·'right' 밖이면 **아무것도 바꾸지 않는다** — 예전에는 검사 없이 캐스팅해
22
34
  // devices·deviceHistory 에 없는 열쇠가 생겼고 prev[side] 가 undefined 라 이력 펼치기에서 터졌다.
23
35
  // 팝업은 닫지 않는다(사용자가 방향을 고르고 다시 제출할 수 있게).
24
36
  const handleDeviceSubmit = useCallback((form) => {
@@ -26,8 +26,8 @@ export const DocumentTitleSuppressContext = createContext(false);
26
26
  /* 제목을 건 화면들 — 나중에 건 것이 위. 떠날 때 **남은 맨 위 제목**으로 돌린다.
27
27
  전에는 저마다 건 순간의 제목(prev)을 기억했다가 되돌려, A 가 떠 있는 동안 B 가 붙고 A 가 나중에 떠나면
28
28
  A 의 prev 로 돌아가 B 의 제목이 지워졌다(말귀 CustomersPage 가 겪고 우회해 둠). */
29
- const 걸린제목 = [];
30
- let 처음제목 = null;
29
+ const titleStack = [];
30
+ let originalTitle = null;
31
31
  /**
32
32
  * 현재 화면의 제목을 `document.title`에 반영한다 — `'<제목> · <앱 이름>'`.
33
33
  *
@@ -47,16 +47,16 @@ export function useDocumentTitle(title) {
47
47
  return;
48
48
  if (typeof title !== 'string' || !title)
49
49
  return;
50
- if (걸린제목.length === 0)
51
- 처음제목 = document.title;
52
- const 칸 = { text: BASE_TITLE ? `${title}${SEP}${BASE_TITLE}` : title };
53
- 걸린제목.push(칸);
54
- document.title = 칸.text;
50
+ if (titleStack.length === 0)
51
+ originalTitle = document.title;
52
+ const entry = { text: BASE_TITLE ? `${title}${SEP}${BASE_TITLE}` : title };
53
+ titleStack.push(entry);
54
+ document.title = entry.text;
55
55
  return () => {
56
- const i = 걸린제목.lastIndexOf(칸);
56
+ const i = titleStack.lastIndexOf(entry);
57
57
  if (i >= 0)
58
- 걸린제목.splice(i, 1);
59
- document.title = 걸린제목.length ? 걸린제목[걸린제목.length - 1].text : (처음제목 ?? document.title);
58
+ titleStack.splice(i, 1);
59
+ document.title = titleStack.length ? titleStack[titleStack.length - 1].text : (originalTitle ?? document.title);
60
60
  };
61
61
  }, [title, suppressed]);
62
62
  }