@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
@@ -43,13 +43,13 @@ export function useEditableList(emptyItem, initialList = []) {
43
43
  /* ํŽธ์ง‘์ด **์ทจ์†Œ๋กœ** ๋๋‚˜๋ฉด ๊ทธ ์‚ฌ์ด ๊ฑด๋„ˆ๋›ด ๋ชฉ๋ก์„ ๋ฐ›๋Š”๋‹ค โ€” ์ „์—๋Š” initialList ๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งŒ ๋Œ์•„, ํŽธ์ง‘ ์ค‘ ๋“ค์–ด์˜จ ์„œ๋ฒ„ ๋ชฉ๋ก์ด
44
44
  ์ทจ์†Œํ•œ ๋’ค์—๋„ ๋๋‚ด ์•ˆ ๋ณด์˜€๋‹ค(๋ฐฉ๋ฌธ ์˜ˆ์ •์„ ๊ณ ์น˜๋‹ค ์ทจ์†Œํ•˜๋ฉด ๊ทธ ์‚ฌ์ด ๋‹ค๋ฅธ ๋ฐฉ๋ฌธ์ด ๋น ์ง„ ์ฑ„). commitEdit ์œผ๋กœ ๋๋‚œ ๋•Œ๋Š”
45
45
  ๋ฐฉ๊ธˆ ์ปค๋ฐ‹ํ•œ ํŽธ์ง‘์„ ์˜› ๋ชฉ๋ก์œผ๋กœ ๋ฎ์ง€ ์•Š๋„๋ก ํ•œ ๋ฒˆ ๋„˜๊ธด๋‹ค(๋‹ค์Œ ๋ชฉ๋ก์ด ์˜ค๋ฉด ๊ทธ๋•Œ ๋ฐ›๋Š”๋‹ค). */
46
- const ์ปค๋ฐ‹์œผ๋กœ๋๋‚จRef = useRef(false);
46
+ const endedByCommitRef = useRef(false);
47
47
  const prevInitialRef = useRef(null);
48
48
  useEffect(() => {
49
49
  if (isEditing)
50
50
  return;
51
- if (์ปค๋ฐ‹์œผ๋กœ๋๋‚จRef.current) {
52
- ์ปค๋ฐ‹์œผ๋กœ๋๋‚จRef.current = false;
51
+ if (endedByCommitRef.current) {
52
+ endedByCommitRef.current = false;
53
53
  return;
54
54
  }
55
55
  const prev = prevInitialRef.current;
@@ -80,7 +80,7 @@ export function useEditableList(emptyItem, initialList = []) {
80
80
  setSelected(updated);
81
81
  // ํŽธ์ง‘ ์ค‘์ด์—ˆ์„ ๋•Œ๋งŒ ํ‘œ์‹œํ•œ๋‹ค(ํŽธ์ง‘์ด ์•„๋‹Œ๋ฐ ํ‘œ์‹œํ•˜๋ฉด ๋‹ค์Œ ๋ชฉ๋ก ํ•œ ๋ฒˆ์„ ํ—›๋˜์ด ๊ฑด๋„ˆ๋›ด๋‹ค). ๊ฐ™์€ ๊ฐ’์„ ๋„ฃ๋Š” ํ‘œ์‹œ๋ผ ๋‘ ๋ฒˆ ๋ถˆ๋ ค๋„ ๊ฐ™๋‹ค
82
82
  setIsEditing(prev => { if (prev)
83
- ์ปค๋ฐ‹์œผ๋กœ๋๋‚จRef.current = true; return false; });
83
+ endedByCommitRef.current = true; return false; });
84
84
  }, []);
85
85
  const remove = useCallback((item) => {
86
86
  setList(prev => prev.filter(v => !isSameEditableItem(v, item)));
@@ -40,8 +40,8 @@ export function useFocusTrap(panelRef, enabled = true) {
40
40
  const active = document.activeElement;
41
41
  /* ํฌ์ปค์Šค๊ฐ€ **ํŒ ์ž์ฒด**(tabIndex=-1 โ€” ๋ณธ๋ฌธ ๊ธ€์ž๋ฅผ ๋ˆ„๋ฅด๋ฉด ํŒ์ด ๋ฐ›๋Š”๋‹ค)์— ์žˆ์œผ๋ฉด ์ฒซ ์นธยท๋ ์นธ ์–ด๋А ์ชฝ๋„ ์•„๋‹ˆ๋ผ
42
42
  ๋˜๊ฐ์ง€ ์•Š์•„, Shift+Tab ํ•œ ๋ฒˆ์— ๋’ค ํ™”๋ฉด์œผ๋กœ ๋‚˜๊ฐ”๋‹ค. ํŒ ์ž์ฒด๋ฉด Shift+Tab ์€ ๋ ์นธ์œผ๋กœ ๋ณด๋‚ธ๋‹ค. */
43
- const ํŒ์ž์ฒด = active === panel;
44
- if (e.shiftKey && (active === first || ํŒ์ž์ฒด)) {
43
+ const onPanelItself = active === panel;
44
+ if (e.shiftKey && (active === first || onPanelItself)) {
45
45
  e.preventDefault();
46
46
  last.focus();
47
47
  }
@@ -5,7 +5,7 @@ export default function useFontScale({ storageKey = 'fontScale',
5
5
  // ๊ธฐ๋ณธ๊ฐ’์„ ์ค‘์•™ํ™”: Training/Tooltip ํ…์ŠคํŠธ ๋ฐ•์Šค ๊ณตํ†ต๊ฐ’
6
6
  baseFont = 20, minScale = 0.8, maxScale = 1.4, step = 0.1, lineHeightRatio = 1.4, } = {}) {
7
7
  const clamp = (v) => Math.max(minScale, Math.min(maxScale, Number(v)));
8
- const ์ฝ๊ธฐ = (key) => {
8
+ const read = (key) => {
9
9
  try {
10
10
  const raw = localStorage.getItem(key);
11
11
  const parsed = raw ? Number(raw) : 1;
@@ -21,13 +21,13 @@ baseFont = 20, minScale = 0.8, maxScale = 1.4, step = 0.1, lineHeightRatio = 1.4
21
21
  return clamp(1);
22
22
  }
23
23
  };
24
- const [scale, setScaleRaw] = useState(() => ์ฝ๊ธฐ(storageKey));
24
+ const [scale, setScaleRaw] = useState(() => read(storageKey));
25
25
  /* ์—ด์‡ ๊ฐ€ ๋ฐ”๋€Œ๋ฉด **๊ทธ ์—ด์‡ ์˜ ์ €์žฅ๊ฐ’** ์œผ๋กœ โ€” ์ „์—๋Š” ์•ž ์—ด์‡ ์˜ ๊ฐ’์„ ๋“ค๊ณ  ์žˆ๋‹ค๊ฐ€ ์•„๋ž˜ ์“ฐ๊ธฐ๊ฐ€ ์ƒˆ ์—ด์‡ ์˜ ์ €์žฅ๊ฐ’์„ ๋ฎ์—ˆ๋‹ค.
26
26
  ๋ Œ๋” ์ค‘์— ๋งž์ถ”๋ฏ€๋กœ ์“ฐ๊ธฐ effect ๋Š” ์ƒˆ ๊ฐ’์œผ๋กœ๋งŒ ๋ˆ๋‹ค. */
27
- const [์ฝ์€์—ด์‡ , set์ฝ์€์—ด์‡ ] = useState(storageKey);
28
- if (์ฝ์€์—ด์‡  !== storageKey) {
29
- set์ฝ์€์—ด์‡ (storageKey);
30
- setScaleRaw(์ฝ๊ธฐ(storageKey));
27
+ const [loadedKey, setLoadedKey] = useState(storageKey);
28
+ if (loadedKey !== storageKey) {
29
+ setLoadedKey(storageKey);
30
+ setScaleRaw(read(storageKey));
31
31
  }
32
32
  useEffect(() => {
33
33
  try {
@@ -5,13 +5,13 @@ import { resolveAssetUrl } from '../utils';
5
5
  /* ์†์„ฑ ๊ฐ’์ด **๋ฌธ์„œ ๋ฐ–์„ ๊ฐ€๋ฆฌํ‚ค๋Š”** url( ์„ ํ’ˆ์—ˆ๋‚˜. `url(#๊ทธ๋ผ๋ฐ์ด์…˜)`ยท`url(#clip)` ์ฒ˜๋Ÿผ ๊ฐ™์€ SVG ์•ˆ์˜ ์กฐ๊ฐ๋งŒ ๊ฐ€๋ฆฌํ‚ค๋Š” ๊ฒƒ์€ ๋‘”๋‹ค โ€”
6
6
  ์ฝ”์–ด ๋กœ๊ณ  ์„œ๋ฅธ์—ฌ์„ฏ ์žฅ์ด clip-pathยทfill ๋กœ ๊ทธ๊ฒƒ์„ ์“ด๋‹ค(๊ฑท์œผ๋ฉด ์ธ๋ผ์ธ์œผ๋กœ ๊ทธ๋ฆฐ ๋กœ๊ณ ๊ฐ€ ๊นจ์ง„๋‹ค).
7
7
  CSS ์ด์Šค์ผ€์ดํ”„๋กœ url ์„ ์ˆจ๊ธด ๊ผด(์—ญ์Šฌ๋ž˜์‹œ + ๊ด„ํ˜ธ)๋„ ๊ฑท๋Š”๋‹ค โ€” ์•„์ด์ฝ˜ ์†์„ฑ์— ๊ทธ๋Ÿฐ ๊ฐ’์„ ์“ธ ๊นŒ๋‹ญ์ด ์—†๋‹ค. */
8
- function ๋ฐ”๊นฅurl์žˆ๋‚˜(value) {
8
+ function hasExternalUrl(value) {
9
9
  if (value.includes('\\') && value.includes('('))
10
10
  return true;
11
11
  if (!/url\s*\(/i.test(value))
12
12
  return false;
13
- const ์กฐ๊ฐ๋งŒ๋บ€ = value.replace(/url\(\s*(['"]?)#[^)'"]*\1\s*\)/gi, '');
14
- return /url\s*\(/i.test(์กฐ๊ฐ๋งŒ๋บ€);
13
+ const withoutFragments = value.replace(/url\(\s*(['"]?)#[^)'"]*\1\s*\)/gi, '');
14
+ return /url\s*\(/i.test(withoutFragments);
15
15
  }
16
16
  function sanitizeSvg(raw) {
17
17
  if (typeof window === 'undefined')
@@ -19,11 +19,11 @@ function sanitizeSvg(raw) {
19
19
  /* ์ธ๋ผ์ธ SVG ์—์„œ **๋ฐ”๊นฅ ์ž์›์„ ๋ถ€๋ฅด๋Š” ๊ธธ** ์„ ๊ฑท๋Š”๋‹ค โ€” `<style>`(@importยทbackground:url) ๊ณผ ๋ฐ”๊นฅ url( ์ด ๋“  ์†์„ฑ(styleยทfillยทfilterโ€ฆ).
20
20
  ๊ทธ๋Œ€๋กœ ๋‘๋ฉด ์•„์ด์ฝ˜ ํ•œ ์žฅ์ด ์ œ3์ž ์ฃผ์†Œ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด๊ฑฐ๋‚˜(์ถ”์ ) CSS ๋กœ ํ™”๋ฉด์„ ๋ฎ์„ ์ˆ˜ ์žˆ์—ˆ๋‹ค.
21
21
  ํ›…์€ DOMPurify ์ „์—ญ์— ๋ถ™์œผ๋ฏ€๋กœ ์ด ํ•œ ๋ฒˆ์˜ sanitize ๋™์•ˆ๋งŒ ๊ฑธ๊ณ  ๊ณง๋ฐ”๋กœ ๋—€๋‹ค(์•ฑ์˜ ๋‹ค๋ฅธ DOMPurify ์“ฐ์ž„์— ์ƒˆ์ง€ ์•Š๊ฒŒ). */
22
- const ํ›… = (_node, data) => {
23
- if (typeof data.attrValue === 'string' && ๋ฐ”๊นฅurl์žˆ๋‚˜(data.attrValue))
22
+ const hook = (_node, data) => {
23
+ if (typeof data.attrValue === 'string' && hasExternalUrl(data.attrValue))
24
24
  data.keepAttr = false;
25
25
  };
26
- DOMPurify.addHook('uponSanitizeAttribute', ํ›…);
26
+ DOMPurify.addHook('uponSanitizeAttribute', hook);
27
27
  try {
28
28
  return DOMPurify.sanitize(raw, {
29
29
  USE_PROFILES: { svg: true, svgFilters: true },
@@ -32,7 +32,7 @@ function sanitizeSvg(raw) {
32
32
  });
33
33
  }
34
34
  finally {
35
- DOMPurify.removeHook('uponSanitizeAttribute', ํ›…);
35
+ DOMPurify.removeHook('uponSanitizeAttribute', hook);
36
36
  }
37
37
  }
38
38
  /** @public */
@@ -42,8 +42,8 @@ export function useListControls(opts = {}) {
42
42
  const [page, setPage] = useState(1);
43
43
  /* ๋ชป ์ฝํžˆ๋Š” ์ชฝํฌ๊ธฐ('' ยท 'all' ยท '0')๋Š” ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ โ€” ๊ทธ ์…ˆ์ด ํ™”๋ฉด๋งˆ๋‹ค ํ•œ ๋ฒŒ์”ฉ ์žˆ์—ˆ๋‹ค.
44
44
  ๊ธฐ๋ณธ๊ฐ’ ์ž์ฒด๊ฐ€ ๋ชป ์ฝํžˆ๋ฉด 25 ๋‹ค. ๋ชฉ๋ก์ด ํ†ต์งธ๋กœ ์‚ฌ๋ผ์ง€๋Š” ๊ฐ’(0ยทNaN)์„ ์—ฌ๊ธฐ์„œ ๋‹ซ๋Š”๋‹ค. */
45
- const ๊ธฐ๋ณธ์ˆ˜ = Number(defaultPageSize) || 25;
46
- const perPage = Number(pageSize) || ๊ธฐ๋ณธ์ˆ˜;
45
+ const defaultSize = Number(defaultPageSize) || 25;
46
+ const perPage = Number(pageSize) || defaultSize;
47
47
  /* **๊ฒ€์ƒ‰์–ด์™€ ์ชฝํฌ๊ธฐ๋ฅผ ์—ฌ๊ธฐ์„œ ๋„ฃ๋Š”๋‹ค** โ€” ๋ถ€๋ฅด๋Š” ์ชฝ์ด ์ ์œผ๋ฉด ๋น ๋œจ๋ฆด ์ˆ˜ ์žˆ๊ณ , ๋น ๋œจ๋ ค๋„ ์•ˆ ๋นจ๊ฐœ์ง„๋‹ค.
48
48
  ์ด์–ด ๋ถ™์ด๋Š” ๊ฒƒ์€ `usePageResetOnChange` ๊ฐ€ ๊ธ€ ํ•˜๋‚˜๋ฅผ ๋ฐ›๊ธฐ ๋•Œ๋ฌธ์ด๊ณ , ๊ฐ’์ด ๊ธ€ยท์ˆ˜ยท์ฐธ๊ฑฐ์ง“๋ฟ์ด๋ผ
49
49
  `[object Object]` ๋กœ ๋ญ‰๊ฐœ์งˆ ์ผ์ด ์—†๋‹ค(ํƒ€์ž…์ด ๋ง‰๋Š”๋‹ค). */
@@ -11,12 +11,12 @@
11
11
  * React ๊ฐ€ ์ด ๊ผด์„ ์œ„ํ•ด ๋‚ด์ค€ ๊ธธ์ด๋‹ค(๊ฐ™์€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐ”๋กœ ๋‹ค์‹œ ๊ทธ๋ฆฐ๋‹ค).
12
12
  *
13
13
  * ```ts
14
- * usePageResetOnChange(`${๊ฒ€์ƒ‰์–ด}|${๊ฐˆ๋ž˜}|${์ •๋ ฌ}`, setPage)
14
+ * usePageResetOnChange(`${๊ฒ€์ƒ‰์–ด}|${๊ฐˆ๋ž˜}|${sort}`, setPage)
15
15
  * const { pageItems, totalPages } = usePagedList(๊ฑธ๋Ÿฌ๋‚ธ๊ฒƒ, ์ชฝํฌ๊ธฐ, page)
16
16
  * ```
17
17
  *
18
- * @param ์—ด์‡  ์กฐ๊ฑด์„ ํ•˜๋‚˜๋กœ ์ด์–ด ๋ถ™์ธ ๊ธ€ โ€” ์ด๊ฒƒ์ด ๋‹ฌ๋ผ์ง€๋ฉด ์ฒซ ์ชฝ์ด๋‹ค
18
+ * @param key ์กฐ๊ฑด์„ ํ•˜๋‚˜๋กœ ์ด์–ด ๋ถ™์ธ ๊ธ€ โ€” ์ด๊ฒƒ์ด ๋‹ฌ๋ผ์ง€๋ฉด ์ฒซ ์ชฝ์ด๋‹ค
19
19
  * @param setPage ์ชฝ ๋ฒˆํ˜ธ๋ฅผ ์ฅ” ์ชฝ์—์„œ ์ค€๋‹ค
20
20
  * @public
21
21
  */
22
- export declare function usePageResetOnChange(์—ด์‡ : string, setPage: (n: number) => void): void;
22
+ export declare function usePageResetOnChange(key: string, setPage: (n: number) => void): void;
@@ -13,18 +13,18 @@ import { useState } from 'react';
13
13
  * React ๊ฐ€ ์ด ๊ผด์„ ์œ„ํ•ด ๋‚ด์ค€ ๊ธธ์ด๋‹ค(๊ฐ™์€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐ”๋กœ ๋‹ค์‹œ ๊ทธ๋ฆฐ๋‹ค).
14
14
  *
15
15
  * ```ts
16
- * usePageResetOnChange(`${๊ฒ€์ƒ‰์–ด}|${๊ฐˆ๋ž˜}|${์ •๋ ฌ}`, setPage)
16
+ * usePageResetOnChange(`${๊ฒ€์ƒ‰์–ด}|${๊ฐˆ๋ž˜}|${sort}`, setPage)
17
17
  * const { pageItems, totalPages } = usePagedList(๊ฑธ๋Ÿฌ๋‚ธ๊ฒƒ, ์ชฝํฌ๊ธฐ, page)
18
18
  * ```
19
19
  *
20
- * @param ์—ด์‡  ์กฐ๊ฑด์„ ํ•˜๋‚˜๋กœ ์ด์–ด ๋ถ™์ธ ๊ธ€ โ€” ์ด๊ฒƒ์ด ๋‹ฌ๋ผ์ง€๋ฉด ์ฒซ ์ชฝ์ด๋‹ค
20
+ * @param key ์กฐ๊ฑด์„ ํ•˜๋‚˜๋กœ ์ด์–ด ๋ถ™์ธ ๊ธ€ โ€” ์ด๊ฒƒ์ด ๋‹ฌ๋ผ์ง€๋ฉด ์ฒซ ์ชฝ์ด๋‹ค
21
21
  * @param setPage ์ชฝ ๋ฒˆํ˜ธ๋ฅผ ์ฅ” ์ชฝ์—์„œ ์ค€๋‹ค
22
22
  * @public
23
23
  */
24
- export function usePageResetOnChange(์—ด์‡ , setPage) {
25
- const [๋ณธ์—ด์‡ , set๋ณธ์—ด์‡ ] = useState(์—ด์‡ );
26
- if (๋ณธ์—ด์‡  !== ์—ด์‡ ) {
27
- set๋ณธ์—ด์‡ (์—ด์‡ );
24
+ export function usePageResetOnChange(key, setPage) {
25
+ const [seenKey, setSeenKey] = useState(key);
26
+ if (seenKey !== key) {
27
+ setSeenKey(key);
28
28
  setPage(1);
29
29
  }
30
30
  }
@@ -17,7 +17,7 @@ export function usePasswordChange(onChangePassword) {
17
17
  const clearTimerRef = useRef(null);
18
18
  /* ์ฒ˜๋ฆฌ ์ค‘์—๋Š” ๋‹ค์‹œ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค โ€” ๋‘ ๋ฒˆ ๋ˆ„๋ฅด๋ฉด ๋ณ€๊ฒฝ์ด ๋‘ ๋ฒˆ ๋‚˜๊ฐ€, ๋จผ์ € ์˜จ ์„ฑ๊ณต ๋’ค์— ๋Šฆ๊ฒŒ ์˜จ "ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ‹€๋ฆผ" ์ด
19
19
  ๋‚จ์•„ ๋ฐ”๋€Œ์—ˆ๋Š”๋ฐ ์˜ค๋ฅ˜ ๋ฌธ๊ตฌ๊ฐ€ ๋–ด๋‹ค(๋ง๊ท€ ๊ณ„์ • ํ™”๋ฉด ๋‹จ์ถ”๋Š” ๋กœ๋”ฉ ์ค‘์—๋„ ๋ˆŒ๋ฆฐ๋‹ค) */
20
- const ์ฒ˜๋ฆฌ์ค‘Ref = useRef(false);
20
+ const busyRef = useRef(false);
21
21
  useEffect(() => {
22
22
  return () => {
23
23
  if (clearTimerRef.current !== null)
@@ -29,7 +29,7 @@ export function usePasswordChange(onChangePassword) {
29
29
  setPwMsg(null);
30
30
  }, []);
31
31
  const handleChangePw = useCallback(async () => {
32
- if (์ฒ˜๋ฆฌ์ค‘Ref.current)
32
+ if (busyRef.current)
33
33
  return;
34
34
  if (!pw.current) {
35
35
  setPwMsg({ type: 'error', text: t('account.security.password.currentRequired', 'ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.') });
@@ -46,7 +46,7 @@ export function usePasswordChange(onChangePassword) {
46
46
  return;
47
47
  }
48
48
  setPwLoading(true);
49
- ์ฒ˜๋ฆฌ์ค‘Ref.current = true;
49
+ busyRef.current = true;
50
50
  // ์ฝœ๋ฐฑ์ด ๋ฌธ๊ตฌ ๋Œ€์‹  **๊ฑฐ์ ˆ**ํ•˜๋ฉด(๋ง ๋Š๊น€ ๋“ฑ) ๋กœ๋”ฉ์ด true ๋กœ ๋‚จ์•„ ๋‹จ์ถ”๊ฐ€ ์˜์˜ ์ž ๊ฒผ๊ณ ,
51
51
  // reset() ๋„ ๋กœ๋”ฉ์€ ์•ˆ ๊ฑด๋“œ๋ ค ํ’€ ๊ธธ์ด ์—†์—ˆ๋‹ค. ๊ฑฐ์ ˆ๋„ ์˜ค๋ฅ˜ ๋ฌธ๊ตฌ๋กœ ๋ณด์ด๊ณ  ๋กœ๋”ฉ์€ ๋Š˜ ๋‚ด๋ฆฐ๋‹ค.
52
52
  let err;
@@ -57,7 +57,7 @@ export function usePasswordChange(onChangePassword) {
57
57
  err = getErrorMessage(e);
58
58
  }
59
59
  finally {
60
- ์ฒ˜๋ฆฌ์ค‘Ref.current = false;
60
+ busyRef.current = false;
61
61
  setPwLoading(false);
62
62
  }
63
63
  if (err) {
@@ -7,19 +7,19 @@ import { useCallback, useEffect, useRef, useState } from 'react';
7
7
  */
8
8
  export function usePendingPress(fn) {
9
9
  const [pending, setPending] = useState(false);
10
- const ์ง„ํ–‰์ค‘ = useRef(false);
11
- const ๋ถ™์–ด์žˆ์Œ = useRef(true);
12
- useEffect(() => { ๋ถ™์–ด์žˆ์Œ.current = true; return () => { ๋ถ™์–ด์žˆ์Œ.current = false; }; }, []);
10
+ const running = useRef(false);
11
+ const mounted = useRef(true);
12
+ useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
13
13
  const press = useCallback(() => {
14
- if (!fn || ์ง„ํ–‰์ค‘.current)
14
+ if (!fn || running.current)
15
15
  return;
16
16
  const r = fn();
17
17
  if (r && typeof r.then === 'function') {
18
- ์ง„ํ–‰์ค‘.current = true;
18
+ running.current = true;
19
19
  setPending(true);
20
- const ํ’€๊ธฐ = () => { ์ง„ํ–‰์ค‘.current = false; if (๋ถ™์–ด์žˆ์Œ.current)
20
+ const settle = () => { running.current = false; if (mounted.current)
21
21
  setPending(false); };
22
- r.then(ํ’€๊ธฐ, ํ’€๊ธฐ);
22
+ r.then(settle, settle);
23
23
  }
24
24
  }, [fn]);
25
25
  return { pending, press };
@@ -3,7 +3,7 @@ import { useCallback } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { getTodayKst, toKstYmd } from '../utils/date';
5
5
  /** 'YYYY-MM-DD' ๋‘ ๋‚ ์งœ ์‚ฌ์ด์˜ ๋‹ฌ๋ ฅ๋‚  ์ˆ˜ */
6
- function ๋‚ ์ˆ˜(from, to) {
6
+ function dayDiff(from, to) {
7
7
  const [fy, fm, fd] = from.split('-').map(Number);
8
8
  const [ty, tm, td] = to.split('-').map(Number);
9
9
  return Math.round((Date.UTC(ty, tm - 1, td) - Date.UTC(fy, fm - 1, fd)) / 86400000);
@@ -23,10 +23,10 @@ export function useRelativeDay() {
23
23
  return useCallback((iso) => {
24
24
  if (!iso)
25
25
  return '';
26
- const ๊ทธ๋‚  = toKstYmd(iso);
27
- if (!๊ทธ๋‚ )
26
+ const ymd = toKstYmd(iso);
27
+ if (!ymd)
28
28
  return '';
29
- const days = ๋‚ ์ˆ˜(๊ทธ๋‚ , getTodayKst());
29
+ const days = dayDiff(ymd, getTodayKst());
30
30
  if (days <= 0)
31
31
  return t('myHomework.relativeDay.today', '์˜ค๋Š˜');
32
32
  if (days === 1)
@@ -26,7 +26,7 @@ export function useSoundCompassExam(speakers, { mode = 'playback', autoStart = f
26
26
  useEffect(() => () => clearTimers(), []);
27
27
  const answerId = queue[index]?.id ?? null;
28
28
  /** ์ด๋ฏธ ๊ณ ๋ฅธ ๋ฌธํ•ญ ๋ฒˆํ˜ธ โ€” ์ƒˆ ๋ฌธํ•ญ์ด ๋Œ€๊ธฐ์— ๋“ค์–ด๊ฐ€๋ฉด ํ’€๋ฆฐ๋‹ค(playQuestion) */
29
- const ๊ณ ๋ฆ„์ž ๊ธˆ = useRef(null);
29
+ const pickLockRef = useRef(null);
30
30
  // โ”€โ”€ playback: ๋ชจ๋“  ์Šคํ”ผ์ปค๋ฅผ ์ˆœ์„œ๋Œ€๋กœ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
31
31
  const runPlayback = useCallback(() => {
32
32
  clearTimers();
@@ -49,7 +49,7 @@ export function useSoundCompassExam(speakers, { mode = 'playback', autoStart = f
49
49
  onDoneRef.current?.({ score: scoreRef.current, total: q.length });
50
50
  return;
51
51
  }
52
- ๊ณ ๋ฆ„์ž ๊ธˆ.current = null;
52
+ pickLockRef.current = null;
53
53
  setIndex(i);
54
54
  setSelectedId(null);
55
55
  setPhase('playing');
@@ -87,7 +87,7 @@ export function useSoundCompassExam(speakers, { mode = 'playback', autoStart = f
87
87
  /* eslint-disable react-hooks/set-state-in-effect */
88
88
  /* ๋ฐฐ์น˜๊ฐ€ ๋ฐ”๋€Œ์—ˆ๋Š”์ง€๋Š” **์Šคํ”ผ์ปค id ๋“ค**๋กœ ๋ณธ๋‹ค โ€” ๊ฐœ์ˆ˜๋งŒ ๋ณด๋ฉด ์ˆ˜๊ฐ€ ๊ฐ™์€ ๋‹ค๋ฅธ ๋ฐฐ์น˜(HORIZON_7 โ†” PRESET_27 ๊ฐ€๋กœ)๋กœ ๋ฐ”๊ฟ”๋„
89
89
  ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•„ ์˜› ๋ฌธ์ œ ์ค„์ด ๋‚จ๊ณ  ์ •๋‹ต id ๊ฐ€ ์ƒˆ ๋ฐฐ์น˜์— ์—†์–ด ๋ฌด์—‡์„ ๊ณจ๋ผ๋„ ์˜ค๋‹ต์ด์—ˆ๋‹ค. */
90
- const ๋ฐฐ์น˜์—ด์‡  = speakers.map((sp) => sp.id).join('|');
90
+ const layoutKey = speakers.map((sp) => sp.id).join('|');
91
91
  useEffect(() => {
92
92
  if (autoStart)
93
93
  start();
@@ -96,14 +96,14 @@ export function useSoundCompassExam(speakers, { mode = 'playback', autoStart = f
96
96
  // ๋ฐฐ์น˜ยท๋ชจ๋“œ๊ฐ€ ๋ฐ”๋€Œ๊ฑฐ๋‚˜ ๋– ๋‚  ๋•Œ ์•ž ํŒ์˜ ์˜ˆ์•ฝ์„ ๊ฑท๋Š”๋‹ค
97
97
  return () => clearTimers();
98
98
  // eslint-disable-next-line react-hooks/exhaustive-deps
99
- }, [mode, ๋ฐฐ์น˜์—ด์‡ , autoStart]);
99
+ }, [mode, layoutKey, autoStart]);
100
100
  /* eslint-enable react-hooks/set-state-in-effect */
101
101
  const onSelectSpeaker = useCallback((id) => {
102
102
  /* ๊ฐ™์€ ๋Œ€๊ธฐ์—์„œ **ํ•œ ๋ฒˆ๋งŒ** ๋ฐ›๋Š”๋‹ค โ€” ๋‹ค์‹œ ๊ทธ๋ ค์ง€๊ธฐ ์ „์— ๊ฐ™์€ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ๋‘ ๋ฒˆ ๋ถˆ๋ฆฌ๋ฉด(๋ Œ๋” ์‹œ์ ์˜ phase ๋งŒ ๋ณด๋ฉด) ์ ์ˆ˜๊ฐ€ +2,
103
103
  ๋‹ค์Œ ๋ฌธ์ œ๊ฐ€ ๋‘ ๋ฒˆ ์˜ˆ์•ฝ๋๋‹ค. ๊ณ ๋ฅด๋Š” ์ˆœ๊ฐ„ ref ๋กœ ๋‹ซ๋Š”๋‹ค. */
104
- if (phase !== 'waiting' || mode === 'playback' || ๊ณ ๋ฆ„์ž ๊ธˆ.current === index)
104
+ if (phase !== 'waiting' || mode === 'playback' || pickLockRef.current === index)
105
105
  return;
106
- ๊ณ ๋ฆ„์ž ๊ธˆ.current = index;
106
+ pickLockRef.current = index;
107
107
  setSelectedId(id);
108
108
  const correct = id === queue[index]?.id;
109
109
  if (correct)
@@ -56,26 +56,26 @@ export interface TableSortOptions<K extends string> {
56
56
  * **์ •๋ ฌ์ด ๋ฐ”๋€Œ๋ฉด ์ฒซ ์ชฝ์œผ๋กœ** โ€” `useTablePaging` ์˜ `resetOn` ์— ๋Œ๋ ค๋ฐ›์€ `์ •๋ ฌ` ์„ ๊ทธ๋Œ€๋กœ ๋„ฃ๋Š”๋‹ค.
57
57
  * `์ •๋ ฌ` ์€ ๋ˆ„๋ฅผ ๋•Œ๋งŒ ์ƒˆ ๊ฐ์ฒด๊ฐ€ ๋˜๋ฏ€๋กœ(๋ Œ๋”๋งˆ๋‹ค ์ƒˆ๋กœ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค) ๊ทธ ์ž์ฒด๊ฐ€ ์—ด์‡ ๋‹ค. ์ชฝ์„ ๋”ฐ๋กœ ๋“ค๊ณ  ์žˆ์œผ๋ฉด `onSortChange` ์—์„œ ๋˜๋Œ๋ฆฐ๋‹ค.
58
58
  *
59
- * `sortRows(rows, ๊ฐ’)` ์€ ์ง€๊ธˆ ์ •๋ ฌ๋กœ ์ค„์„ ์„ธ์šด **์ƒˆ ๋ฐฐ์—ด**์„ ์ค€๋‹ค(์ •๋ ฌ์ด ์—†์œผ๋ฉด ๋ฐ›์€ ๋ฐฐ์—ด ๊ทธ๋Œ€๋กœ) โ€” `์นธ์ˆœ`(์ˆซ์ž๋Š” ์ˆ˜๋กœ,
59
+ * `sortRows(rows, getValue)` ์€ ์ง€๊ธˆ ์ •๋ ฌ๋กœ ์ค„์„ ์„ธ์šด **์ƒˆ ๋ฐฐ์—ด**์„ ์ค€๋‹ค(์ •๋ ฌ์ด ์—†์œผ๋ฉด ๋ฐ›์€ ๋ฐฐ์—ด ๊ทธ๋Œ€๋กœ) โ€” `compareCell`(์ˆซ์ž๋Š” ์ˆ˜๋กœ,
60
60
  * ํ•œ๊ธ€ยท์˜๋ฌธ์€ ์‚ฌ๋žŒ ์ฐจ๋ก€) ์œผ๋กœ ๊ฒฌ์ฃผ๊ณ  **๋นˆ ๊ฐ’(nullยทundefinedยท'')์€ ์˜ค๋ฆ„ยท๋‚ด๋ฆผ ๋ชจ๋‘ ๋งจ ๋’ค**(ListPopup ๊ณผ ๊ฐ™์€ ๊ทœ์น™).
61
61
  *
62
62
  * @example
63
- * const { ์ •๋ ฌ, ์นธ, sortRows } = useTableSort<'name' | 'joined'>()
63
+ * const { sort, field, sortRows } = useTableSort<'name' | 'joined'>()
64
64
  * const ์„ธ์šด์ค„ = useMemo(() => sortRows(rows, (r, key) => r[key]), [rows, sortRows])
65
- * const { pageRows, pageSizeProps, paginationProps } = useTablePaging(์„ธ์šด์ค„, ์ •๋ ฌ) // ์ •๋ ฌ์ด ๋ฐ”๋€Œ๋ฉด ์ฒซ ์ชฝ
66
- * <Table fields={[์นธ('์ด๋ฆ„', 'name'), ์นธ('๊ฐ€์ž…์ผ', 'joined')]} โ€ฆ />
65
+ * const { pageRows, pageSizeProps, paginationProps } = useTablePaging(์„ธ์šด์ค„, sort) // ์ •๋ ฌ์ด ๋ฐ”๋€Œ๋ฉด ์ฒซ ์ชฝ
66
+ * <Table fields={[field('์ด๋ฆ„', 'name'), field('๊ฐ€์ž…์ผ', 'joined')]} โ€ฆ />
67
67
  *
68
68
  * @public
69
69
  */
70
70
  export declare function useTableSort<K extends string>(opts?: TableSortOptions<K>): {
71
- ์ •๋ ฌ: {
71
+ sort: {
72
72
  key: K;
73
73
  asc: boolean;
74
74
  } | null;
75
- ์นธ: (label: string, key: K) => {
75
+ field: (label: string, key: K) => {
76
76
  label: string;
77
77
  sortDir: "desc" | "asc" | null;
78
78
  onClick: () => void;
79
79
  };
80
- sortRows: <R>(rows: readonly R[], ๊ฐ’: (row: R, key: K) => unknown) => readonly R[];
80
+ sortRows: <R>(rows: readonly R[], getValue: (row: R, key: K) => unknown) => readonly R[];
81
81
  };
@@ -2,7 +2,7 @@
2
2
  import { useCallback, useMemo, useRef, useState } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { VAP_PAGE_SIZE_OPTIONS } from './useVisitAllPanel';
5
- import { ์นธ์ˆœ๋นˆ๊ฐ’๋’ค } from '../utils/textSort';
5
+ import { compareCellBlankLast } from '../utils/textSort';
6
6
  /**
7
7
  * ํ‘œ์˜ ์ชฝ๋„˜๊น€ โ€” ๊ฐœ์ˆ˜ ๋ณด๊ธฐ ยท ์ชฝ ยท ์ด๋ฒˆ ์ชฝ ์ค„.
8
8
  *
@@ -26,17 +26,17 @@ import { ์นธ์ˆœ๋นˆ๊ฐ’๋’ค } from '../utils/textSort';
26
26
  export function useTablePaging(rows, resetOn, opts) {
27
27
  const { t } = useTranslation();
28
28
  const [pageSize, setPageSizeRaw] = useState(() => {
29
- const ์ฒ˜์Œ = opts?.initialPageSize;
30
- return ์ฒ˜์Œ && VAP_PAGE_SIZE_OPTIONS.some(o => o.value === ์ฒ˜์Œ) ? ์ฒ˜์Œ : '25';
29
+ const initial = opts?.initialPageSize;
30
+ return initial && VAP_PAGE_SIZE_OPTIONS.some(o => o.value === initial) ? initial : '25';
31
31
  });
32
32
  const [page, setPage] = useState(1);
33
33
  /* ๋Š˜์–ด๋†“๋Š” ์ฐจ๋ก€๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์ฒซ ์ชฝ์œผ๋กœ โ€” 3์ชฝ์„ ๋ณด๋˜ ์ค‘์— ์ •๋ ฌํ•˜๋ฉด ๋ฌด์—‡์ด ์œ„๋กœ ์™”๋Š”์ง€
34
34
  ๋ชป ๋ณธ๋‹ค. ๋ Œ๋” ์ค‘์— ๋˜๋Œ๋ฆฐ๋‹ค(์ €์žฅ์†Œ์˜ ๋‹ค๋ฅธ ๊ณณ๋“ค๊ณผ ๊ฐ™์€ ๊ผด: ๊ฐ’์ด ๋ฐ”๋€ ๊ทธ ๋ Œ๋”์—์„œ
35
35
  ๋ฐ”๋กœ ๋งž์ถฐ์•ผ ํ•œ ๋ฐ•์ž ๋Šฆ์€ ํ™”๋ฉด์ด ์•ˆ ๋œฌ๋‹ค). */
36
- const [๋ณธ๊ฒƒ, set๋ณธ๊ฒƒ] = useState(resetOn);
36
+ const [seen, setSeen] = useState(resetOn);
37
37
  // Object.is โ€” NaN ์ด๋ฉด `!==` ๊ฐ€ ๋Š˜ ์ฐธ์ด๋ผ ๋ Œ๋”๊ฐ€ ๋๋‚˜์ง€ ์•Š์•˜๋‹ค. ๊ฐ์ฒด๋Š” ๋ Œ๋”๋งˆ๋‹ค ์ƒˆ๋กœ ๋งŒ๋“ค๋ฉด ๋งค๋ฒˆ ๋ฐ”๋€ ๊ฒƒ์ด๋‹ค(์›์‹œ๊ฐ’์„ ๋„˜๊ธธ ๊ฒƒ)
38
- if (!Object.is(๋ณธ๊ฒƒ, resetOn)) {
39
- set๋ณธ๊ฒƒ(resetOn);
38
+ if (!Object.is(seen, resetOn)) {
39
+ setSeen(resetOn);
40
40
  if (page !== 1)
41
41
  setPage(1);
42
42
  }
@@ -50,14 +50,14 @@ export function useTablePaging(rows, resetOn, opts) {
50
50
  // 1 ๋ณด๋‹ค ์ž‘์€ ์ชฝ(setPage(0)ยท-1)๋„ ๋ง‰๋Š”๋‹ค โ€” 0 ์€ ๋นˆ ์ชฝ, -1 ์€ ์Œ์ˆ˜ slice ๋กœ ์—‰๋šฑํ•œ ์ค„์ด ๋‚˜์™”๋‹ค
51
51
  if (page > total || page < 1 || !Number.isFinite(page))
52
52
  setPage(Math.min(Math.max(1, Number.isFinite(page) ? page : 1), total));
53
- const ์ด์ชฝ = Math.min(Math.max(1, Number.isFinite(page) ? page : 1), total);
54
- const pageRows = useMemo(() => rows.slice((์ด์ชฝ - 1) * Number(pageSize), ์ด์ชฝ * Number(pageSize)), [rows, ์ด์ชฝ, pageSize]);
53
+ const clampedPage = Math.min(Math.max(1, Number.isFinite(page) ? page : 1), total);
54
+ const pageRows = useMemo(() => rows.slice((clampedPage - 1) * Number(pageSize), clampedPage * Number(pageSize)), [rows, clampedPage, pageSize]);
55
55
  return {
56
56
  pageRows,
57
57
  /** Toolbar ์˜ pageSize ์ž๋ฆฌ์— ๊ทธ๋Œ€๋กœ ๋„ฃ๋Š”๋‹ค */
58
58
  pageSizeProps: { options: pageSizeOptions, value: pageSize, onChange: setPageSize },
59
59
  /** Toolbar ์˜ pagination ์ž๋ฆฌ์— ๊ทธ๋Œ€๋กœ ๋„ฃ๋Š”๋‹ค */
60
- paginationProps: { page: ์ด์ชฝ, total, onChange: setPage },
60
+ paginationProps: { page: clampedPage, total, onChange: setPage },
61
61
  setPage,
62
62
  };
63
63
  }
@@ -67,43 +67,49 @@ export function useTablePaging(rows, resetOn, opts) {
67
67
  * **์ •๋ ฌ์ด ๋ฐ”๋€Œ๋ฉด ์ฒซ ์ชฝ์œผ๋กœ** โ€” `useTablePaging` ์˜ `resetOn` ์— ๋Œ๋ ค๋ฐ›์€ `์ •๋ ฌ` ์„ ๊ทธ๋Œ€๋กœ ๋„ฃ๋Š”๋‹ค.
68
68
  * `์ •๋ ฌ` ์€ ๋ˆ„๋ฅผ ๋•Œ๋งŒ ์ƒˆ ๊ฐ์ฒด๊ฐ€ ๋˜๋ฏ€๋กœ(๋ Œ๋”๋งˆ๋‹ค ์ƒˆ๋กœ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค) ๊ทธ ์ž์ฒด๊ฐ€ ์—ด์‡ ๋‹ค. ์ชฝ์„ ๋”ฐ๋กœ ๋“ค๊ณ  ์žˆ์œผ๋ฉด `onSortChange` ์—์„œ ๋˜๋Œ๋ฆฐ๋‹ค.
69
69
  *
70
- * `sortRows(rows, ๊ฐ’)` ์€ ์ง€๊ธˆ ์ •๋ ฌ๋กœ ์ค„์„ ์„ธ์šด **์ƒˆ ๋ฐฐ์—ด**์„ ์ค€๋‹ค(์ •๋ ฌ์ด ์—†์œผ๋ฉด ๋ฐ›์€ ๋ฐฐ์—ด ๊ทธ๋Œ€๋กœ) โ€” `์นธ์ˆœ`(์ˆซ์ž๋Š” ์ˆ˜๋กœ,
70
+ * `sortRows(rows, getValue)` ์€ ์ง€๊ธˆ ์ •๋ ฌ๋กœ ์ค„์„ ์„ธ์šด **์ƒˆ ๋ฐฐ์—ด**์„ ์ค€๋‹ค(์ •๋ ฌ์ด ์—†์œผ๋ฉด ๋ฐ›์€ ๋ฐฐ์—ด ๊ทธ๋Œ€๋กœ) โ€” `compareCell`(์ˆซ์ž๋Š” ์ˆ˜๋กœ,
71
71
  * ํ•œ๊ธ€ยท์˜๋ฌธ์€ ์‚ฌ๋žŒ ์ฐจ๋ก€) ์œผ๋กœ ๊ฒฌ์ฃผ๊ณ  **๋นˆ ๊ฐ’(nullยทundefinedยท'')์€ ์˜ค๋ฆ„ยท๋‚ด๋ฆผ ๋ชจ๋‘ ๋งจ ๋’ค**(ListPopup ๊ณผ ๊ฐ™์€ ๊ทœ์น™).
72
72
  *
73
73
  * @example
74
- * const { ์ •๋ ฌ, ์นธ, sortRows } = useTableSort<'name' | 'joined'>()
74
+ * const { sort, field, sortRows } = useTableSort<'name' | 'joined'>()
75
75
  * const ์„ธ์šด์ค„ = useMemo(() => sortRows(rows, (r, key) => r[key]), [rows, sortRows])
76
- * const { pageRows, pageSizeProps, paginationProps } = useTablePaging(์„ธ์šด์ค„, ์ •๋ ฌ) // ์ •๋ ฌ์ด ๋ฐ”๋€Œ๋ฉด ์ฒซ ์ชฝ
77
- * <Table fields={[์นธ('์ด๋ฆ„', 'name'), ์นธ('๊ฐ€์ž…์ผ', 'joined')]} โ€ฆ />
76
+ * const { pageRows, pageSizeProps, paginationProps } = useTablePaging(์„ธ์šด์ค„, sort) // ์ •๋ ฌ์ด ๋ฐ”๋€Œ๋ฉด ์ฒซ ์ชฝ
77
+ * <Table fields={[field('์ด๋ฆ„', 'name'), field('๊ฐ€์ž…์ผ', 'joined')]} โ€ฆ />
78
78
  *
79
79
  * @public
80
80
  */
81
81
  export function useTableSort(opts) {
82
- const [์ •๋ ฌ, ์ •๋ ฌ๋ฐ”๊พธ๊ธฐ] = useState(null);
82
+ const [sortState, setSortState] = useState(null);
83
83
  /* ๋‹ค์Œ ์ •๋ ฌ์€ **ref ์—์„œ** ๊ณ„์‚ฐํ•ด ์•Œ๋ฆฐ๋‹ค โ€” ๊ฐฑ์‹  ํ•จ์ˆ˜ ์•ˆ์—์„œ ๋ถ€๋ฅด๋ฉด StrictMode ๊ฐ€ ๋‘ ๋ฒˆ ๋ถ€๋ฅธ๋‹ค */
84
- const ์ง€๊ธˆ์ •๋ ฌ = useRef(์ •๋ ฌ);
85
- const ์•Œ๋ฆผ = useRef(opts?.onSortChange);
86
- ์•Œ๋ฆผ.current = opts?.onSortChange;
87
- const ๋ˆ„๋ฆ„ = useCallback((key) => {
88
- const ์•ž = ์ง€๊ธˆ์ •๋ ฌ.current;
89
- const ๋‹ค์Œ = ์•ž?.key === key ? { key, asc: !์•ž.asc } : { key, asc: true };
90
- ์ง€๊ธˆ์ •๋ ฌ.current = ๋‹ค์Œ;
91
- ์ •๋ ฌ๋ฐ”๊พธ๊ธฐ(๋‹ค์Œ);
92
- ์•Œ๋ฆผ.current?.(๋‹ค์Œ);
84
+ const sortRef = useRef(sortState);
85
+ const onChangeRef = useRef(opts?.onSortChange);
86
+ onChangeRef.current = opts?.onSortChange;
87
+ const onHeadClick = useCallback((key) => {
88
+ const prev = sortRef.current;
89
+ const next = prev?.key === key ? { key, asc: !prev.asc } : { key, asc: true };
90
+ sortRef.current = next;
91
+ setSortState(next);
92
+ onChangeRef.current?.(next);
93
93
  }, []);
94
94
  /** Table ์˜ field ํ•˜๋‚˜์— ๋ถ™์ผ ๊ฒƒ โ€” ๋ผ๋ฒจ๊ณผ ํ•จ๊ป˜ ์“ด๋‹ค */
95
- const ์นธ = useCallback((label, key) => ({
95
+ const fieldHead = useCallback((label, key) => ({
96
96
  label,
97
- sortDir: ์ •๋ ฌ?.key === key ? (์ •๋ ฌ.asc ? 'asc' : 'desc') : null,
98
- onClick: () => ๋ˆ„๋ฆ„(key),
99
- }), [์ •๋ ฌ, ๋ˆ„๋ฆ„]);
97
+ sortDir: sortState?.key === key ? (sortState.asc ? 'asc' : 'desc') : null,
98
+ onClick: () => onHeadClick(key),
99
+ }), [sortState, onHeadClick]);
100
100
  /** ์ง€๊ธˆ ์ •๋ ฌ๋กœ ์„ธ์šด ์ƒˆ ๋ฐฐ์—ด(์ •๋ ฌ ์—†์œผ๋ฉด ๋ฐ›์€ ๋ฐฐ์—ด ๊ทธ๋Œ€๋กœ). ๋นˆ ๊ฐ’์€ ์˜ค๋ฆ„ยท๋‚ด๋ฆผ ๋ชจ๋‘ ๋งจ ๋’ค. */
101
- const sortRows = useCallback((rows, ๊ฐ’) => {
102
- if (!์ •๋ ฌ)
101
+ const sortRows = useCallback((rows, getValue) => {
102
+ if (!sortState)
103
103
  return rows;
104
- const { key, asc } = ์ •๋ ฌ;
105
- return [...rows].sort((a, b) => ์นธ์ˆœ๋นˆ๊ฐ’๋’ค(๊ฐ’(a, key), ๊ฐ’(b, key), asc));
106
- }, [์ •๋ ฌ]);
107
- /* ์ชฝ๋„˜๊น€์— ๊ทธ๋Œ€๋กœ ๊ฑด๋„จ๋‹ค โ€” ์ฐจ๋ก€๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์ฒซ ์ชฝ์œผ๋กœ ๋Œ์•„๊ฐ€๊ฒŒ */
108
- return { ์ •๋ ฌ, ์นธ, sortRows };
104
+ const { key, asc } = sortState;
105
+ return [...rows].sort((a, b) => compareCellBlankLast(getValue(a, key), getValue(b, key), asc));
106
+ }, [sortState]);
107
+ /* ์ชฝ๋„˜๊น€์— ๊ทธ๋Œ€๋กœ ๊ฑด๋„จ๋‹ค โ€” ์ฐจ๋ก€๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์ฒซ ์ชฝ์œผ๋กœ ๋Œ์•„๊ฐ€๊ฒŒ.
108
+ **์˜› ํ•œ๊ธ€ ์ด๋ฆ„(`์ •๋ ฌ`ยท`์นธ`)์€ 2026-10-13 ๊ฑท์—ˆ๋‹ค** โ€” ์†Œ๋น„์ž๊ฐ€ ๋‹ค ์˜ฎ๊ธด ๊ฒƒ์„ ์„ธ์–ด ๋ณด๊ณ  ๊ฑท์—ˆ๋‹ค
109
+ (๋ง๊ท€์˜ ์ฝ”์–ด `useTableSort` ์ž๋ฆฌ ์—ด, ์ „๋ถ€ `{ sort, field }`. kscanยท์†Œ๋ฆฌ๋ฐฉํ–ฅ์„ฑยทfe ๋Š” 0). */
110
+ return {
111
+ sort: sortState,
112
+ field: fieldHead,
113
+ sortRows,
114
+ };
109
115
  }
@@ -54,72 +54,72 @@ import { logTrainingTime } from '../utils/trainingLogger';
54
54
  * @public
55
55
  */
56
56
  /** ์ง€๊ธˆ ๊ทธ ๊ฐˆ๋ž˜์˜ ์‹œ๊ฐ„์„ ์ ๊ณ  ์žˆ๋Š” ์ฃผ์ธ โ€” ๊ฐˆ๋ž˜๋งˆ๋‹ค ํ•˜๋‚˜(์œ„ ๋จธ๋ฆฌ๋ง ์ฐธ๊ณ ) */
57
- const ์ ๋Š”์ฃผ์ธ = new Set();
57
+ const writers = new Set();
58
58
  export default function useTrainingSessionLog(trainingType) {
59
59
  useEffect(() => {
60
60
  if (!trainingType)
61
61
  return; // ํƒ€์ž… ๋ฏธ์ƒ์ด๋ฉด ๊ธฐ๋กํ•˜์ง€ ์•Š์Œ
62
62
  /* **๊ฐˆ๋ž˜๋งˆ๋‹ค ์ฃผ์ธ์€ ํ•˜๋‚˜๋‹ค.** ์ด๋ฏธ ๋ˆ„๊ฐ€ ์ ๊ณ  ์žˆ์œผ๋ฉด ์กฐ์šฉํžˆ ๋ฌผ๋Ÿฌ๋‚œ๋‹ค โ€” ์•ˆ ๊ทธ๋Ÿฌ๋ฉด
63
63
  ๋‘ ์ฐฝ์ด ์ œ ์‹œ๊ฐ„์„ ๋”ฐ๋กœ ์ ์–ด ๊ฒน์นœ ๋งŒํผ์ด ๋‘ ๋ฒˆ ์„ธ์–ด์ง„๋‹ค. */
64
- if (์ ๋Š”์ฃผ์ธ.has(trainingType))
64
+ if (writers.has(trainingType))
65
65
  return;
66
- ์ ๋Š”์ฃผ์ธ.add(trainingType);
67
- const ๋ณด์ž„ = () => typeof document === 'undefined' || document.visibilityState !== 'hidden';
68
- let ์ฒ˜์Œ๋ณด์ž„ = null;
69
- let ๊ตฌ๊ฐ„์‹œ์ž‘ = null;
70
- let ๋ชจ์€ms = 0;
71
- let ๋ณด๋‚ธms = 0; // ์ง€๊ธˆ๊นŒ์ง€ ์ค„๋กœ ๋‚ด๋ณด๋‚ธ ๋ˆ„๊ณ„ โ€” ๋‹ค์Œ ์ค„์€ ์—ฌ๊ธฐ์„œ๋ถ€ํ„ฐ ์ด์–ด ๋ถ™์ธ๋‹ค
72
- const ๊ตฌ๊ฐ„์—ด๊ธฐ = () => { const t = Date.now(); if (์ฒ˜์Œ๋ณด์ž„ == null)
73
- ์ฒ˜์Œ๋ณด์ž„ = t; if (๊ตฌ๊ฐ„์‹œ์ž‘ == null)
74
- ๊ตฌ๊ฐ„์‹œ์ž‘ = t; };
75
- const ๊ตฌ๊ฐ„๋‹ซ๊ธฐ = () => { if (๊ตฌ๊ฐ„์‹œ์ž‘ != null) {
76
- ๋ชจ์€ms += Date.now() - ๊ตฌ๊ฐ„์‹œ์ž‘;
77
- ๊ตฌ๊ฐ„์‹œ์ž‘ = null;
66
+ writers.add(trainingType);
67
+ const isVisible = () => typeof document === 'undefined' || document.visibilityState !== 'hidden';
68
+ let firstVisibleAt = null;
69
+ let spanStartedAt = null;
70
+ let accumulatedMs = 0;
71
+ let sentMs = 0; // ์ง€๊ธˆ๊นŒ์ง€ ์ค„๋กœ ๋‚ด๋ณด๋‚ธ ๋ˆ„๊ณ„ โ€” ๋‹ค์Œ ์ค„์€ ์—ฌ๊ธฐ์„œ๋ถ€ํ„ฐ ์ด์–ด ๋ถ™์ธ๋‹ค
72
+ const openSpan = () => { const t = Date.now(); if (firstVisibleAt == null)
73
+ firstVisibleAt = t; if (spanStartedAt == null)
74
+ spanStartedAt = t; };
75
+ const closeSpan = () => { if (spanStartedAt != null) {
76
+ accumulatedMs += Date.now() - spanStartedAt;
77
+ spanStartedAt = null;
78
78
  } };
79
- if (๋ณด์ž„())
80
- ๊ตฌ๊ฐ„์—ด๊ธฐ();
79
+ if (isVisible())
80
+ openSpan();
81
81
  /* ์•„์ง ์•ˆ ๋ณด๋‚ธ ๋งŒํผ์„ ํ•œ ์ค„๋กœ. ๋ ์‹œ๊ฐ์€ ์ฒ˜์Œ ๋ณด์ธ ๋•Œ + ๋ชจ์€ ์‹œ๊ฐ„, ์‹œ์ž‘ ์‹œ๊ฐ์€ ์•ž ์ค„์ด
82
82
  ๋๋‚œ ์ž๋ฆฌ โ€” ์ค„๋ผ๋ฆฌ ๊ฒน์น˜์ง€ ์•Š์œผ๋‹ˆ ํ•ฉ์‚ฐํ•ด๋„ ์ด์ค‘์œผ๋กœ ์„ธ์ง€ ์•Š๋Š”๋‹ค.
83
83
  ์ „๋‹ฌ์€ ๋“ฑ๋ก๋œ ๋กœ๊ฑฐ(logTrainingTime)๋กœ ๊ฐ„๋‹ค. ์ฝ”์–ด๋Š” ๋ณด๋‚ผ ์ฃผ์†Œ๋ฅผ ๋ชจ๋ฅด๋ฏ€๋กœ
84
84
  navigator.sendBeacon ์„ ์—ฌ๊ธฐ์„œ ์ง์ ‘ ์“ธ ์ˆ˜ ์—†๋‹ค โ€” ์•ฑ์ด ๋“ฑ๋กํ•˜๋Š” ๋กœ๊ฑฐ์˜ ๋ชซ์ด๋‹ค. */
85
- const ๋ณด๋‚ด๊ธฐ = () => {
86
- ๊ตฌ๊ฐ„๋‹ซ๊ธฐ();
87
- if (์ฒ˜์Œ๋ณด์ž„ == null)
85
+ const send = () => {
86
+ closeSpan();
87
+ if (firstVisibleAt == null)
88
88
  return;
89
- const ๋‚จ์€ms = ๋ชจ์€ms - ๋ณด๋‚ธms;
89
+ const pendingMs = accumulatedMs - sentMs;
90
90
  /* 1์ดˆ๊ฐ€ ์•ˆ ๋˜๋Š” ์กฐ๊ฐ์€ ๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค โ€” ์„œ๋ฒ„(log_training_category_time)๊ฐ€ 1์ดˆ ๋ฏธ๋งŒ ์ค„์„ ๋ฒ„๋ฆฌ๋ฏ€๋กœ
91
91
  ๋ถ€๋ฅด๊ธฐ ์ œํ•œ(10๋ถ„ 60๋ฒˆ)๋งŒ ์ถ•๋‚ธ๋‹ค(๋ง๊ท€ ํ™•์ธ 2026-09-16). */
92
- if (๋‚จ์€ms < 1000)
92
+ if (pendingMs < 1000)
93
93
  return;
94
94
  // ์ฒซ ์ค„์—๋งŒ 5์ดˆ ๋ฌธํ„ฑ์„ ๊ฑด๋‹ค(์ž˜๋ชป ์—ด์—ˆ๋‹ค ๋‹ซ์€ ํŒ). ํ•œ ๋ฒˆ ๋ณด๋‚ธ ํŒ์€ ์ด๋ฏธ ์‹ค์žฌํ•˜๋Š”
95
95
  // ํ›ˆ๋ จ์ด๋ผ, ๋Œ์•„์™€ ๋” ํ•œ ์‹œ๊ฐ„์€ ์งง์•„๋„ ๋ฒ„๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.
96
- if (๋ณด๋‚ธms === 0 && ๋ชจ์€ms < 5000)
96
+ if (sentMs === 0 && accumulatedMs < 5000)
97
97
  return;
98
- const ์‹œ์ž‘ms = ์ฒ˜์Œ๋ณด์ž„ + ๋ณด๋‚ธms;
99
- ๋ณด๋‚ธms = ๋ชจ์€ms;
98
+ const startMs = firstVisibleAt + sentMs;
99
+ sentMs = accumulatedMs;
100
100
  logTrainingTime({
101
101
  trainingType,
102
- startedAt: new Date(์‹œ์ž‘ms).toISOString(),
103
- endedAt: new Date(์ฒ˜์Œ๋ณด์ž„ + ๋ชจ์€ms).toISOString(),
102
+ startedAt: new Date(startMs).toISOString(),
103
+ endedAt: new Date(firstVisibleAt + accumulatedMs).toISOString(),
104
104
  });
105
105
  };
106
106
  // ์ˆจ์œผ๋ฉด ๋ณด๋‚ธ๋‹ค(์•ฑ์ด ๊ทธ๋Œ€๋กœ ์ฃฝ์„ ์ˆ˜ ์žˆ๋‹ค). ๋‹ค์‹œ ๋ณด์ด๋ฉด ์ด์–ด์„œ ๋ชจ์€๋‹ค.
107
- const ๋ณด์ž„๋ฐ”๋€œ = () => { if (๋ณด์ž„())
108
- ๊ตฌ๊ฐ„์—ด๊ธฐ();
107
+ const onVisibilityChange = () => { if (isVisible())
108
+ openSpan();
109
109
  else
110
- ๋ณด๋‚ด๊ธฐ(); };
110
+ send(); };
111
111
  if (typeof document !== 'undefined')
112
- document.addEventListener('visibilitychange', ๋ณด์ž„๋ฐ”๋€œ);
112
+ document.addEventListener('visibilitychange', onVisibilityChange);
113
113
  // ํŽ˜์ด์ง€๊ฐ€ ๋‚ด๋ ค๊ฐ€๋ฉด(ํƒญยท์•ฑ ๋‹ซ๊ธฐ) React ์ •๋ฆฌ๊ฐ€ ์•ˆ ๋ˆ๋‹ค โ€” ๊ทธ๋•Œ ๋ณด๋‚ธ๋‹ค
114
114
  if (typeof window !== 'undefined')
115
- window.addEventListener('pagehide', ๋ณด๋‚ด๊ธฐ);
115
+ window.addEventListener('pagehide', send);
116
116
  return () => {
117
117
  if (typeof document !== 'undefined')
118
- document.removeEventListener('visibilitychange', ๋ณด์ž„๋ฐ”๋€œ);
118
+ document.removeEventListener('visibilitychange', onVisibilityChange);
119
119
  if (typeof window !== 'undefined')
120
- window.removeEventListener('pagehide', ๋ณด๋‚ด๊ธฐ);
121
- ๋ณด๋‚ด๊ธฐ();
122
- ์ ๋Š”์ฃผ์ธ.delete(trainingType);
120
+ window.removeEventListener('pagehide', send);
121
+ send();
122
+ writers.delete(trainingType);
123
123
  };
124
124
  }, [trainingType]);
125
125
  }
@@ -13,10 +13,10 @@ import { useEditableList, isSameEditableItem } from './useEditableList';
13
13
  * ์—†์—ˆ๋‹ค** โ€” V8 ์˜ TimSort ๋Š” ๊ฐ™์€ ๊ฐ’ ๊ตฌ๊ฐ„์—์„œ ๋น„๊ต๋ฅผ ์•„์˜ˆ ์•ˆ ๋ถˆ๋Ÿฌ, ๋’ค์„ž์ธ ์ž๋ฃŒ๋กœ
14
14
  * ์žฌ ๋ด๋„ ๊ฒฐ๊ณผ๊ฐ€ ๊ฐ™์•˜๋‹ค(์‹ค์ธก). ๊ทธ๋ž˜์„œ ์ด ํ•œ ๊ฐ€์ง€๋Š” ์‹œํ—˜์œผ๋กœ ๋ชป ๋ฐ•๋Š”๋‹ค.
15
15
  */
16
- const ์ •๋ ฌํ‚ค = (v) => `${v.year.padStart(4, '0')}-${v.month.padStart(2, '0')}-${v.day.padStart(2, '0')}` +
16
+ const sortKeyOf = (v) => `${v.year.padStart(4, '0')}-${v.month.padStart(2, '0')}-${v.day.padStart(2, '0')}` +
17
17
  ` ${v.hour.padStart(2, '0')}:${v.minute.padStart(2, '0')}`;
18
18
  const sortByDate = (list) => [...list].sort((a, b) => {
19
- const x = ์ •๋ ฌํ‚ค(a), y = ์ •๋ ฌํ‚ค(b);
19
+ const x = sortKeyOf(a), y = sortKeyOf(b);
20
20
  return x < y ? -1 : x > y ? 1 : 0;
21
21
  });
22
22
  const emptyScheduleItem = { year: '', month: '', day: '', hour: '09', minute: '00', memo: '' };
@@ -34,8 +34,8 @@ initialSchedule = []) {
34
34
  useMemo ๋กœ ๋ฌถ๋Š” ๊ฒƒ์€ ์„ฑ๋Šฅ ๋•Œ๋ฌธ์ด ์•„๋‹ˆ๋ผ **๋˜๋Œ์ด๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•ด์„œ๋‹ค** โ€”
35
35
  useEditableList ๋Š” ์ดˆ๊ธฐ ๋ชฉ๋ก์„ ์›์†Œ ์ฐธ์กฐ๋กœ ๊ฒฌ์ฃผ๋Š”๋ฐ, ๋ Œ๋”๋งˆ๋‹ค ์ƒˆ ๋ฐฐ์—ด์„ ๋งŒ๋“ค๋ฉด
36
36
  ์›์†Œ๋Š” ๊ฐ™์•„๋„ ๋งค๋ฒˆ setList ๊ฐ€ ๋Œ์•„ ๋ฌดํ•œ ๋ Œ๋”๊ฐ€ ๋œ๋‹ค. */
37
- const ์ •๋ ฌ๋œ์ดˆ๊ธฐ๋ชฉ๋ก = useMemo(() => sortByDate(initialSchedule), [initialSchedule]);
38
- const { list: scheduleList, setList: setScheduleList, selected: selectedSchedule, setSelected: setSelectedSchedule, isEditing: isEditingSchedule, setIsEditing: setIsEditingSchedule, editForm: editScheduleForm, setEditForm: setEditScheduleForm, openDetail: openScheduleDetail, commitEdit, remove, } = useEditableList(emptyScheduleItem, ์ •๋ ฌ๋œ์ดˆ๊ธฐ๋ชฉ๋ก);
37
+ const sortedInitial = useMemo(() => sortByDate(initialSchedule), [initialSchedule]);
38
+ const { list: scheduleList, setList: setScheduleList, selected: selectedSchedule, setSelected: setSelectedSchedule, isEditing: isEditingSchedule, setIsEditing: setIsEditingSchedule, editForm: editScheduleForm, setEditForm: setEditScheduleForm, openDetail: openScheduleDetail, commitEdit, remove, } = useEditableList(emptyScheduleItem, sortedInitial);
39
39
  /** ๋ฏธ๋ฐฉ๋ฌธ ์ฒ˜๋ฆฌ๋œ ์˜ˆ์•ฝ โ€” ์‚ญ์ œํ•˜์ง€ ์•Š๊ณ  ๋ณด์กดํ•ด ํ†ตํ•ฉ ๋ฐฉ๋ฌธ ๋ชฉ๋ก์—์„œ ๋ฑƒ์ง€๋กœ ํ‘œ์‹œ */
40
40
  const [noShowList, setNoShowList] = useState([]);
41
41
  const { year: ty, month: tm, day: td } = today();