@ihabdevteam/core 0.322.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 (196) hide show
  1. package/CHANGELOG.md +517 -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 +54 -16
  8. package/dist/packages/core/src/components/Accordion.js +3 -3
  9. package/dist/packages/core/src/components/Alert.js +2 -2
  10. package/dist/packages/core/src/components/BarChart.js +120 -119
  11. package/dist/packages/core/src/components/BottomNav.js +18 -18
  12. package/dist/packages/core/src/components/BulkBar.js +25 -25
  13. package/dist/packages/core/src/components/Button.js +16 -16
  14. package/dist/packages/core/src/components/Calendar.js +17 -17
  15. package/dist/packages/core/src/components/ChartSection.js +30 -30
  16. package/dist/packages/core/src/components/CountTile.js +38 -38
  17. package/dist/packages/core/src/components/DatePicker.js +59 -59
  18. package/dist/packages/core/src/components/Dialog.js +14 -14
  19. package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.js +8 -8
  20. package/dist/packages/core/src/components/FilterPanel.js +6 -6
  21. package/dist/packages/core/src/components/FilterSectionInfo.js +6 -6
  22. package/dist/packages/core/src/components/GameIntro.js +1 -1
  23. package/dist/packages/core/src/components/Icon.js +2 -2
  24. package/dist/packages/core/src/components/ItemActions.js +16 -16
  25. package/dist/packages/core/src/components/LandingHeader.js +6 -6
  26. package/dist/packages/core/src/components/LineChart.js +24 -24
  27. package/dist/packages/core/src/components/Loader.js +5 -5
  28. package/dist/packages/core/src/components/NoticeBox.js +3 -3
  29. package/dist/packages/core/src/components/PageTitle.js +2 -2
  30. package/dist/packages/core/src/components/Pagination.js +4 -4
  31. package/dist/packages/core/src/components/Panel.js +2 -2
  32. package/dist/packages/core/src/components/PanelRow.js +6 -13
  33. package/dist/packages/core/src/components/PassCard/PassCard.js +9 -9
  34. package/dist/packages/core/src/components/PassCard/passCardProduct.js +2 -2
  35. package/dist/packages/core/src/components/PaymentPlan.js +5 -5
  36. package/dist/packages/core/src/components/Popup/ConfirmPopup.js +29 -29
  37. package/dist/packages/core/src/components/Popup/CouponInputPopup.js +2 -2
  38. package/dist/packages/core/src/components/Popup/DeviceSideTable.js +18 -25
  39. package/dist/packages/core/src/components/Popup/ListPopup.js +3 -3
  40. package/dist/packages/core/src/components/Popup/LoadTablePopup.js +7 -7
  41. package/dist/packages/core/src/components/Popup/Popup.js +27 -27
  42. package/dist/packages/core/src/components/Popup/SaveTablePopup.js +5 -5
  43. package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +4 -4
  44. package/dist/packages/core/src/components/Popup/customerDetailUtils.js +18 -18
  45. package/dist/packages/core/src/components/Popup/useDiscardGuard.js +4 -4
  46. package/dist/packages/core/src/components/ProgressIndicator.js +5 -5
  47. package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +16 -16
  48. package/dist/packages/core/src/components/Rating.js +20 -20
  49. package/dist/packages/core/src/components/Report/ReportDocument.js +21 -21
  50. package/dist/packages/core/src/components/RingChart.js +5 -5
  51. package/dist/packages/core/src/components/SectionHeadFloat.js +15 -15
  52. package/dist/packages/core/src/components/SegmentedControl.js +34 -34
  53. package/dist/packages/core/src/components/SideNav.js +37 -37
  54. package/dist/packages/core/src/components/Slider.js +2 -2
  55. package/dist/packages/core/src/components/SoundCompass.js +5 -5
  56. package/dist/packages/core/src/components/Table.js +257 -98
  57. package/dist/packages/core/src/components/Tabs.js +12 -12
  58. package/dist/packages/core/src/components/TextField.js +34 -34
  59. package/dist/packages/core/src/components/Toast.js +6 -6
  60. package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +4 -4
  61. package/dist/packages/core/src/components/Tooltip/Tooltip.js +22 -22
  62. package/dist/packages/core/src/components/Tooltip/TrainingTooltip.js +4 -4
  63. package/dist/packages/core/src/components/TopBanner.js +28 -28
  64. package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +11 -11
  65. package/dist/packages/core/src/components/Training/CategoryIcon.js +2 -2
  66. package/dist/packages/core/src/components/Training/NoisePlayerContext.js +12 -12
  67. package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +2 -2
  68. package/dist/packages/core/src/components/Training/TrainingBlock.js +11 -11
  69. package/dist/packages/core/src/components/Training/TrainingBlockButton.js +10 -10
  70. package/dist/packages/core/src/components/Training/TrainingBlockGrid.js +21 -21
  71. package/dist/packages/core/src/components/Training/TrainingCard.css +13 -1
  72. package/dist/packages/core/src/components/Training/TrainingCard.js +2 -2
  73. package/dist/packages/core/src/components/Training/TrainingCardGroup.js +6 -6
  74. package/dist/packages/core/src/components/Training/TrainingPagePopup.js +81 -81
  75. package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +17 -17
  76. package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +127 -127
  77. package/dist/packages/core/src/components/Training/TrainingStepCard.js +18 -18
  78. package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +4 -4
  79. package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +12 -12
  80. package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +12 -12
  81. package/dist/packages/core/src/components/Training/TrainingStepTitle.js +4 -4
  82. package/dist/packages/core/src/components/Training/TrainingWordCard.js +17 -17
  83. package/dist/packages/core/src/components/Training/WordTablePopup.js +5 -5
  84. package/dist/packages/core/src/components/Training/audioPlaybackRules.js +8 -8
  85. package/dist/packages/core/src/components/Training/audioTimeline.js +25 -25
  86. package/dist/packages/core/src/components/Training/oneShotSound.js +13 -13
  87. package/dist/packages/core/src/components/Training/trainingMissStats.js +20 -20
  88. package/dist/packages/core/src/components/Training/trainingPopupUtils.js +7 -7
  89. package/dist/packages/core/src/components/Training/useAudioPreview.js +15 -15
  90. package/dist/packages/core/src/components/Training/useFloatingFocus.js +14 -14
  91. package/dist/packages/core/src/components/Training/useNoiseFade.js +22 -22
  92. package/dist/packages/core/src/components/Training/useObjectUrls.js +2 -2
  93. package/dist/packages/core/src/components/Training/usePlaybackSequencer.d.ts +2 -2
  94. package/dist/packages/core/src/components/Training/usePlaybackSequencer.js +23 -23
  95. package/dist/packages/core/src/components/Training/useProgressTicker.js +33 -33
  96. package/dist/packages/core/src/components/Training/useTTSPlayback.js +17 -17
  97. package/dist/packages/core/src/components/Training/useWaveReady.js +11 -11
  98. package/dist/packages/core/src/components/UpdateAlert.js +6 -6
  99. package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +7 -1
  100. package/dist/packages/core/src/components/Word/TableFilterPanel.js +3 -3
  101. package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +10 -10
  102. package/dist/packages/core/src/components/Word/WordTableBlock.js +28 -28
  103. package/dist/packages/core/src/components/Word/WordTableButton.css +16 -2
  104. package/dist/packages/core/src/components/Word/WordTableThumbnail.css +6 -1
  105. package/dist/packages/core/src/components/Word/generatedDraft.js +14 -14
  106. package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -7
  107. package/dist/packages/core/src/components/Word/wordScoreStats.js +15 -15
  108. package/dist/packages/core/src/components/Word/wordTablePrint.js +10 -10
  109. package/dist/packages/core/src/components/Word/wordTableRecommend.js +11 -11
  110. package/dist/packages/core/src/components/Word/wordTableReview.js +28 -28
  111. package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +8 -11
  112. package/dist/packages/core/src/components/Word/wordTableSettings.js +48 -36
  113. package/dist/packages/core/src/components/WordTable.js +130 -130
  114. package/dist/packages/core/src/components/barChartData.d.ts +16 -1
  115. package/dist/packages/core/src/components/barChartData.js +49 -29
  116. package/dist/packages/core/src/components/barChartTypes.js +8 -8
  117. package/dist/packages/core/src/components/tableTrackList.js +11 -11
  118. package/dist/packages/core/src/components/updateAlertStack.js +28 -28
  119. package/dist/packages/core/src/components/useBarChartControl.js +4 -6
  120. package/dist/packages/core/src/components/useComboboxSuggestions.js +17 -17
  121. package/dist/packages/core/src/components/useToastLifecycle.js +18 -18
  122. package/dist/packages/core/src/components/wordTableLayout.d.ts +17 -4
  123. package/dist/packages/core/src/components/wordTableLayout.js +5 -5
  124. package/dist/packages/core/src/context/I18nProvider.js +6 -6
  125. package/dist/packages/core/src/hooks/focusReturn.js +27 -27
  126. package/dist/packages/core/src/hooks/inputModality.js +13 -13
  127. package/dist/packages/core/src/hooks/touchToggle.js +39 -39
  128. package/dist/packages/core/src/hooks/useAnchoredMenu.js +30 -30
  129. package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +28 -28
  130. package/dist/packages/core/src/hooks/useDocumentTitle.js +10 -10
  131. package/dist/packages/core/src/hooks/useEditableList.js +4 -4
  132. package/dist/packages/core/src/hooks/useFocusTrap.js +2 -2
  133. package/dist/packages/core/src/hooks/useFontScale.js +6 -6
  134. package/dist/packages/core/src/hooks/useIcon.js +7 -7
  135. package/dist/packages/core/src/hooks/useListControls.js +2 -2
  136. package/dist/packages/core/src/hooks/usePageResetOnChange.js +3 -3
  137. package/dist/packages/core/src/hooks/usePasswordChange.js +4 -4
  138. package/dist/packages/core/src/hooks/usePendingPress.js +7 -7
  139. package/dist/packages/core/src/hooks/useRelativeDay.js +4 -4
  140. package/dist/packages/core/src/hooks/useSoundCompassExam.js +6 -6
  141. package/dist/packages/core/src/hooks/useTablePaging.d.ts +0 -11
  142. package/dist/packages/core/src/hooks/useTablePaging.js +29 -34
  143. package/dist/packages/core/src/hooks/useTrainingSessionLog.js +35 -35
  144. package/dist/packages/core/src/hooks/useVisitSchedule.js +4 -4
  145. package/dist/packages/core/src/hooks/useWordAttributeFilter.js +14 -14
  146. package/dist/packages/core/src/hooks/useWordMeanings.js +5 -5
  147. package/dist/packages/core/src/hooks/useWordTrainingFilter.js +7 -7
  148. package/dist/packages/core/src/i18n/locales/en.json +13 -2
  149. package/dist/packages/core/src/i18n/locales/ko.json +12 -1
  150. package/dist/packages/core/src/types/wordSearch.d.ts +9 -0
  151. package/dist/packages/core/src/utils/audioMeter.js +5 -5
  152. package/dist/packages/core/src/utils/audioPeaks.d.ts +1 -1
  153. package/dist/packages/core/src/utils/audioPeaks.js +18 -18
  154. package/dist/packages/core/src/utils/blockGrid.js +4 -4
  155. package/dist/packages/core/src/utils/chunkSkew.js +9 -9
  156. package/dist/packages/core/src/utils/clipboard.js +3 -3
  157. package/dist/packages/core/src/utils/dailySelectionUtils.js +12 -12
  158. package/dist/packages/core/src/utils/date.js +12 -12
  159. package/dist/packages/core/src/utils/devWarn.js +4 -4
  160. package/dist/packages/core/src/utils/deviceKind.d.ts +37 -7
  161. package/dist/packages/core/src/utils/deviceKind.js +53 -10
  162. package/dist/packages/core/src/utils/errorMessage.js +7 -7
  163. package/dist/packages/core/src/utils/filterPanelSummary.js +14 -14
  164. package/dist/packages/core/src/utils/format.js +4 -4
  165. package/dist/packages/core/src/utils/getTrainingData.js +9 -9
  166. package/dist/packages/core/src/utils/imageConfig.js +17 -17
  167. package/dist/packages/core/src/utils/koreanSyllable.js +18 -18
  168. package/dist/packages/core/src/utils/navScroll.js +2 -2
  169. package/dist/packages/core/src/utils/phonemeConvert.d.ts +7 -0
  170. package/dist/packages/core/src/utils/phonemeConvert.js +19 -10
  171. package/dist/packages/core/src/utils/phonemeGroups.js +2 -2
  172. package/dist/packages/core/src/utils/phonemePairSearch.d.ts +1 -0
  173. package/dist/packages/core/src/utils/phonemePairSearch.js +118 -102
  174. package/dist/packages/core/src/utils/phonemeSectionCodes.d.ts +52 -0
  175. package/dist/packages/core/src/utils/phonemeSectionCodes.js +76 -0
  176. package/dist/packages/core/src/utils/prefetchChunk.js +6 -6
  177. package/dist/packages/core/src/utils/safeHref.js +4 -4
  178. package/dist/packages/core/src/utils/savedWordTables.js +5 -5
  179. package/dist/packages/core/src/utils/speak.js +12 -12
  180. package/dist/packages/core/src/utils/textSort.d.ts +0 -10
  181. package/dist/packages/core/src/utils/textSort.js +38 -48
  182. package/dist/packages/core/src/utils/theme.js +3 -3
  183. package/dist/packages/core/src/utils/trainingItemResults.js +3 -3
  184. package/dist/packages/core/src/utils/ttsConfig.js +2 -2
  185. package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +45 -45
  186. package/dist/packages/core/src/utils/wordContrast.js +3 -3
  187. package/dist/packages/core/src/utils/wordPairIndex.js +4 -4
  188. package/dist/packages/core/src/utils/wordTrainingGenerators.js +4 -4
  189. package/dist/packages/core/src/utils/wordTrainingPool.js +15 -15
  190. package/dist/packages/core/src/utils/wordTrainingRuntime.js +6 -6
  191. package/dist/packages/core/src/utils/wornDate.d.ts +3 -3
  192. package/dist/packages/core/src/utils/wornDate.js +14 -14
  193. package/dist/packages/core/src/version.d.ts +1 -1
  194. package/dist/packages/core/src/version.js +1 -1
  195. package/dist/utils.cjs.js +4 -4
  196. package/package.json +1 -1
@@ -83,17 +83,17 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
83
83
  const [stateLocal, setStateLocal] = useState('active');
84
84
  /* ์ดˆ์ ์„ ๊ฐ€์ง„ ํฐ '๋“ฃ๊ธฐ' ๋‹จ์ถ”๋ฅผ ๋ˆ„๋ฅด๋ฉด ๊ทธ ๋‹จ์ถ”๊ฐ€ ์‚ฌ๋ผ์ง„๋‹ค โ€” ์žฌ์ƒ ๋ชจ๋“œ๊ฐ€ ๋˜๋ฉด ์ดˆ์ ์„ '์žฌ์ƒ / ๋ฉˆ์ถค' ์œผ๋กœ ์˜ฎ๊ธด๋‹ค
85
85
  (์˜ฎ๊ธฐ์ง€ ์•Š์œผ๋ฉด ์ดˆ์ ์ด body ๋กœ ๋–จ์–ด์ ธ ํ‚ค๋ณด๋“œ ์‚ฌ์šฉ์ž๊ฐ€ ๋ฉˆ์ถœ ๋‹จ์ถ”๋ฅผ ๋‹ค์‹œ ์ฐพ์•„์•ผ ํ–ˆ๋‹ค). */
86
- const ๋“ฃ๊ธฐ์ดˆ์ ์˜ฎ๊ธฐ๊ธฐRef = useRef(false);
86
+ const moveFocusToListenRef = useRef(false);
87
87
  useEffect(() => {
88
- if (stateLocal !== 'playMode' || !๋“ฃ๊ธฐ์ดˆ์ ์˜ฎ๊ธฐ๊ธฐRef.current)
88
+ if (stateLocal !== 'playMode' || !moveFocusToListenRef.current)
89
89
  return;
90
- ๋“ฃ๊ธฐ์ดˆ์ ์˜ฎ๊ธฐ๊ธฐRef.current = false;
91
- const ์ง€๊ธˆ = typeof document !== 'undefined' ? document.activeElement : null;
92
- if (์ง€๊ธˆ && ์ง€๊ธˆ !== document.body)
90
+ moveFocusToListenRef.current = false;
91
+ const active = typeof document !== 'undefined' ? document.activeElement : null;
92
+ if (active && active !== document.body)
93
93
  return;
94
- const ์žฌ์ƒ๋ฉˆ์ถค = btnsRef.current?.querySelectorAll('button')[1];
94
+ const playPauseBtn = btnsRef.current?.querySelectorAll('button')[1];
95
95
  try {
96
- ์žฌ์ƒ๋ฉˆ์ถค?.focus();
96
+ playPauseBtn?.focus();
97
97
  }
98
98
  catch (e) { }
99
99
  }, [stateLocal]);
@@ -107,7 +107,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
107
107
  * ๊ทธ๋ž˜์„œ ๋ฒˆํ˜ธ๋Š” `currentIndex` ์—์„œ ์ฝ๊ณ , ์—ฌ๊ธฐ์„œ๋Š” **๋ฐํž์ง€ ๋ง์ง€๋งŒ** ๋“ ๋‹ค.
108
108
  * ๋‘˜์„ ๋”ฐ๋กœ ๋“ค๋ฉด ์–ธ์  ๊ฐ€ ํ•œ์ชฝ๋งŒ ๊ฐฑ์‹ ๋˜๋Š” ์ž๋ฆฌ๊ฐ€ ์ƒ๊ธด๋‹ค(์‹ค์ œ๋กœ `goToTrack` ์ด
109
109
  * ๋ฒˆํ˜ธ๋งŒ ๋ฐ”๊พธ๊ณ  ์ด ๊ฐ’์€ ํšจ๊ณผ์— ๊ธฐ๋Œ€๊ณ  ์žˆ์—ˆ๋‹ค). */
110
- const [๊ตฌ๊ฐ„ํ‘œ์‹œ, set๊ตฌ๊ฐ„ํ‘œ์‹œ] = useState(false);
110
+ const [showRange, setShowRange] = useState(false);
111
111
  const [totalDuration, setTotalDuration] = useState(0);
112
112
  // ๋ฌธ์žฅ(๋ฉ”์ธ) ์žฌ์ƒ ์†๋„ยท๋ณผ๋ฅจ โ€” ํ•˜๋‹จ ์†Œ์Œ์„ค์ • ํˆดํŒ๊ณผ ๊ณต์œ (NoisePlayerContext)
113
113
  const playbackRate = noisePlayer?.playbackRate ?? 1.0;
@@ -138,16 +138,16 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
138
138
  /* **์‹œ๊ฐ„ํ‘œ์˜ ์ž๋ฆฌยท์˜๋„ยท๋œธ ํƒ€์ด๋จธยท๋ฉˆ์ถ˜ ์ž๋ฆฌ๋Š” ์ง„ํ–‰๊ธฐ๊ฐ€ ์ฅ”๋‹ค**((85) 2a~2c, 2026-10-06).
139
139
  ๋ฌป๋Š” ์ชฝ(์†Œ์Œ ยท ํ‹ฑ ยท ํ† ๊ธ€ ยท ์ž๋ฌผ์‡ )์€ **์ž๋ฆฌ๋ฅผ ๋ฐ›์•„ ์ œ ์†์œผ๋กœ ์…ˆํ•˜์ง€ ์•Š๊ณ ** ์ง„ํ–‰๊ธฐ์—๊ฒŒ
140
140
  ๋ฌป๋Š”๋‹ค โ€” `isRunning()` ํ•˜๋‚˜๋‹ค. ๊ทธ ์…ˆ์ด ์„ธ ๋ฒŒ๋กœ ํฉ์–ด์ ธ ์žˆ๋˜ ๊ฒƒ์ด ์˜ค๋Š˜ ๊ฒฐํ•จ ์—ฌ์„ฏ์˜ ๋ฟŒ๋ฆฌ์˜€๋‹ค. */
141
- const ์ง„ํ–‰๊ธฐ = usePlaybackSequencer({ mountedRef });
141
+ const sequencer = usePlaybackSequencer({ mountedRef });
142
142
  /* `onEnded` ๊ฐ€ ์ ์–ด ๋‘๋Š” "์—ฌ๊ธฐ์„œ๋ถ€ํ„ฐ ์ด์–ด ๊ฐ„๋‹ค" โ€” ํ† ๊ธ€์ด ๋ฉˆ์ถœ ๋•Œ ์ง„ํ–‰๊ธฐ์— ๋„˜๊ธด๋‹ค */
143
- const ์ด์–ด๊ฐ€๊ธฐRefBox = useRef(null);
143
+ const continueRef = useRef(null);
144
144
  /* ์ด๋ฒคํŠธ ์†์žก์ด๋Š” ํ•œ ๋ฒˆ๋งŒ ๋ถ™์œผ๋ฏ€๋กœ prop ์„ ref ๋กœ ๋“ค๊ณ  ๋ณธ๋‹ค โ€” ์•ˆ ๊ทธ๋Ÿฌ๋ฉด ๋‚ก์€ ๊ฐ’์„ ์“ด๋‹ค
145
145
  (์ด ํŒŒ์ผ์ด `disabledRef`ยท`noiseSettingsRef` ์— ์“ฐ๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ ๊ฒฐ). */
146
146
  const leadInMsRef = useRef(leadInMs);
147
147
  const tailMsRef = useRef(tailMs);
148
148
  useEffect(() => { leadInMsRef.current = Math.max(0, leadInMs || 0); }, [leadInMs]);
149
149
  useEffect(() => { tailMsRef.current = Math.max(0, tailMs || 0); }, [tailMs]);
150
- const { start: startTicker, stop: stopTicker, settleTo: settleTicker, } = useProgressTicker({ audioRef, fillRef, waveProgressRef, sourcesRef, currentIndexRef, sequencer: ์ง„ํ–‰๊ธฐ, leadInMs, tailMs });
150
+ const { start: startTicker, stop: stopTicker, settleTo: settleTicker, } = useProgressTicker({ audioRef, fillRef, waveProgressRef, sourcesRef, currentIndexRef, sequencer: sequencer, leadInMs, tailMs });
151
151
  /* ํŒŒํ˜•์ด ์ž๋ฆฌ๋ฅผ ์žก์„ ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๊ทœ์น™์€ useWaveReady ๊ฐ€ ๋“ค๊ณ  ์žˆ๋‹ค โ€”
152
152
  ref ๋‘˜ยทํ•จ์ˆ˜ ๋‘˜์— "์ค€๋น„ ์•ˆ ๋จ์œผ๋กœ ๋˜๋Œ๋ฆฌ๊ธฐ"๊ฐ€ ์ผ๊ณฑ ๊ตฐ๋ฐ ํฉ์–ด์ ธ ์žˆ๋˜ ๊ฒƒ์„ ๋ชจ์•˜๋‹ค. */
153
153
  const { onReadyChange: handleWaveReadyChange, waitForReady: waitForWaveReady, markNotReady: markWaveNotReady, } = useWaveReady({ showSoundWaveForm, showNoiseWaveForm });
@@ -174,9 +174,9 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
174
174
  noisePlayer,
175
175
  audioRef,
176
176
  playbackStateRef,
177
- sequencer: ์ง„ํ–‰๊ธฐ,
177
+ sequencer: sequencer,
178
178
  });
179
- const { startTTS: ํ•ฉ์„ฑ์‹œ์ž‘, stopTTS } = useTTSPlayback({
179
+ const { startTTS: beginTTS, stopTTS } = useTTSPlayback({
180
180
  ttsText,
181
181
  playbackRate,
182
182
  // TTS ์Œ์„ฑ = ๋ฌธ์ œ ์†Œ๋ฆฌ = ์ „์ฒด ์†Œ๋ฆฌ
@@ -201,11 +201,11 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
201
201
  setNoticeMessage(t('training.ttsUnavailable', '์ด ๊ธฐ๊ธฐ์—์„œ๋Š” ์Œ์„ฑ์„ ๋“ค๋ ค์ค„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'));
202
202
  }
203
203
  catch (_) { }
204
- ํ•ฉ์„ฑ์‹œ์ž‘(text);
204
+ beginTTS(text);
205
205
  return;
206
206
  }
207
- ์†Œ๋ฆฌ์žก๊ธฐ('tts');
208
- ํ•ฉ์„ฑ์‹œ์ž‘(text);
207
+ acquireSound('tts');
208
+ beginTTS(text);
209
209
  };
210
210
  const requestedSources = Array.isArray(soundSrc)
211
211
  ? soundSrc.filter(Boolean)
@@ -228,17 +228,17 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
228
228
  *
229
229
  * ์ด์ œ ์„ธ๋Œ€ ํ•˜๋‚˜๋‹ค. `๋ฉˆ์ถค์˜๋„()` ๋Š” ์„ธ๋Œ€๋ฅผ ์˜ฌ๋ฆฌ๊ณ  ์ž๋ฆฌ๋ฅผ ๋น„์šด๋‹ค.
230
230
  */
231
- const ๋ฉˆ์ถค์˜๋„ = () => { ์ง„ํ–‰๊ธฐ.abort(); };
231
+ const abortIntent = () => { sequencer.abort(); };
232
232
  /* ์Šคํ”ผ์ปค๋Š” ํ•˜๋‚˜(currentSound.ts) โ€” ํ•œ ํ™”๋ฉด์˜ ๋‹ค๋ฅธ ์žฌ์ƒ๊ธฐ(๋ฌธ์žฅ ๋“ฃ๊ธฐยท๋ฌธ์ œ ๋“ฃ๊ธฐ)ยท๋‚ฑ๋ง ์นด๋“œยท๋ฏธ๋ฆฌ ๋“ฃ๊ธฐ๊ฐ€ ์šธ๋ฆฌ๋ฉด ์ด ์žฌ์ƒ๊ธฐ๋Š”
233
233
  ๋ฉˆ์ถ˜๋‹ค. ์ „์—๋Š” ๋‚ฑ๋ง ์นด๋“œ๋งŒ ์ž๋ฌผ์‡ ๋ฅผ ์žก์•„, ๋“ฃ๊ธฐ ๋‹จ๊ณ„๊ฐ€ ๋‘˜์ธ ํ™”๋ฉดยท๋“ฃ๊ธฐ+์นด๋“œ ๋‹จ๊ณ„์—์„œ ๋‘ ์†Œ๋ฆฌ๊ฐ€ ํ•จ๊ป˜ ์šธ๋ ธ๋‹ค.
234
234
  ๋ฉˆ์ถค ๋‹จ์ถ”์™€ ๊ฐ™๊ฒŒ ๋ฉˆ์ถ˜๋‹ค(์ด์–ด ๋“ฃ๊ธฐ ๊ฐ€๋Šฅ). ํ•จ์ˆ˜๋Š” ํ•œ ๋ฒŒ์ด์–ด์•ผ ์ž๋ฌผ์‡ ๊ฐ€ '๋‚ด ๊ฒƒ' ์„ ์•Œ์•„๋ณธ๋‹ค. */
235
235
  const stopTTSRef = useRef(null);
236
- const ์†Œ๋ฆฌ์ข…๋ฅ˜Ref = useRef(null);
237
- const ์†Œ๋ฆฌ๋†“๊ธฐRef = useRef(null);
238
- const ์†Œ๋ฆฌ๋ฉˆ์ถคRef = useRef(null);
239
- if (!์†Œ๋ฆฌ๋ฉˆ์ถคRef.current) {
240
- ์†Œ๋ฆฌ๋ฉˆ์ถคRef.current = () => {
241
- ์†Œ๋ฆฌ๋†“๊ธฐRef.current = null;
236
+ const soundKindRef = useRef(null);
237
+ const releaseSoundRef = useRef(null);
238
+ const stopSoundRef = useRef(null);
239
+ if (!stopSoundRef.current) {
240
+ stopSoundRef.current = () => {
241
+ releaseSoundRef.current = null;
242
242
  const a = audioRef.current;
243
243
  /* **๋œธ ์ค‘์ด๋ฉด ์š”์†Œ๋Š” ๋ฉˆ์ถฐ ์žˆ๋‹ค** โ€” `!a.paused` ๋งŒ ๋ณด๋ฉด ์ด ๋ฌธ์„ ์•ˆ ์ง€๋‚˜, ๋‚จ์ด ์Šคํ”ผ์ปค๋ฅผ
244
244
  ๊ฐ€์ ธ๊ฐ€๋„ **๋œธ์€ ์ œ ๊ฒƒ์ธ ์ค„ ์•Œ๊ณ  ๊นจ์–ด๋‚˜ ์†Œ๋ฆฌ๋ฅผ ๋ƒˆ๋‹ค**(๊ฐ€์งœ ์‹œ๊ณ„ ์‹ค์ธก, ๋‘ ๊ธธ ๋‹ค:
@@ -247,8 +247,8 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
247
247
  ์šธ๋ฆฐ๋‹ค" ๊ณ  ๋ณด๊ณ ํ•œ ๊ทธ ์ž๋ฆฌ์˜ **๋œธ ํŒ**์ด๋‹ค.
248
248
  ์˜ค๋Š˜ ๊ฐ™์€ ๊ผด์„ ์—ฌ์„ฏ ๋ฒˆ์งธ ๋งŒ๋‚œ๋‹ค(์†Œ์Œ ๊ฐ์‹œ ยท ์žฌ์ƒํŒ ยท ํ† ๊ธ€ ยท ๊ทธ ํ† ๊ธ€์˜ ๋‘ ๋ฒˆ์งธ ๋ฒŒ ยท
249
249
  ๋ฒˆํ˜ธ ๋‹จ์ถ” ยท ์—ฌ๊ธฐ). ์ด์ œ `isRunning()` ํ•˜๋‚˜๋กœ ๋ฌป๋Š”๋‹ค. */
250
- if (์†Œ๋ฆฌ์ข…๋ฅ˜Ref.current === 'audio' && a && (!a.paused || ์ง„ํ–‰๊ธฐ.isRunning())) {
251
- ๋ฉˆ์ถค์˜๋„();
250
+ if (soundKindRef.current === 'audio' && a && (!a.paused || sequencer.isRunning())) {
251
+ abortIntent();
252
252
  try {
253
253
  a.pause();
254
254
  }
@@ -259,7 +259,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
259
259
  fadeOutNoise();
260
260
  }
261
261
  }
262
- else if (์†Œ๋ฆฌ์ข…๋ฅ˜Ref.current === 'tts') {
262
+ else if (soundKindRef.current === 'tts') {
263
263
  try {
264
264
  stopTTSRef.current?.();
265
265
  }
@@ -267,14 +267,14 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
267
267
  }
268
268
  };
269
269
  }
270
- const ์†Œ๋ฆฌ์žก๊ธฐ = (์ข…๋ฅ˜) => {
271
- ์†Œ๋ฆฌ์ข…๋ฅ˜Ref.current = ์ข…๋ฅ˜;
272
- ์†Œ๋ฆฌ๋†“๊ธฐRef.current = claimSound(์†Œ๋ฆฌ๋ฉˆ์ถคRef.current);
270
+ const acquireSound = (kind) => {
271
+ soundKindRef.current = kind;
272
+ releaseSoundRef.current = claimSound(stopSoundRef.current);
273
273
  };
274
274
  // ๋ฉˆ์ถคยท์ •์ง€ยท์™„๋ฃŒ ๋’ค์—๋Š” ์ž๋ฆฌ๋ฅผ ๋น„์šด๋‹ค โ€” ๋‚จ์ด ์šธ๋ฆด ๋•Œ ๋๋‚œ ์žฌ์ƒ๊ธฐ์˜ ์ƒํƒœ๋ฅผ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ฒŒ
275
- const ์†Œ๋ฆฌ๋†“๊ธฐ = () => { ์†Œ๋ฆฌ๋†“๊ธฐRef.current?.(); ์†Œ๋ฆฌ๋†“๊ธฐRef.current = null; };
276
- useEffect(() => () => { if (์†Œ๋ฆฌ๋ฉˆ์ถคRef.current)
277
- stopSoundIfOwner(์†Œ๋ฆฌ๋ฉˆ์ถคRef.current); }, []);
275
+ const releaseSound = () => { releaseSoundRef.current?.(); releaseSoundRef.current = null; };
276
+ useEffect(() => () => { if (stopSoundRef.current)
277
+ stopSoundIfOwner(stopSoundRef.current); }, []);
278
278
  /* ์ด๋ฒคํŠธ ์†์žก์ดยท๋น„๋™๊ธฐ ํ๋ฆ„ ์•ˆ์—์„œ **์ง€๊ธˆ ๊ฐ’**์„ ์ฝ์–ด์•ผ ํ•œ๋‹ค โ€” ํšจ๊ณผ๊ฐ€ ๋‹ค์‹œ ๋Œ ๋•Œ๊นŒ์ง€
279
279
  ๊ธฐ๋‹ค๋ฆฌ๋ฉด ์ด๋ฏธ ์‹ค์€ ์†Œ๋ฆฌ์— ์˜› ๋น ๋ฅด๊ธฐ๊ฐ€ ๊ฑธ๋ฆฐ๋‹ค(์ด ํŒŒ์ผ์˜ `disabledRef`ยท`leadInMsRef` ์™€ ๊ฐ™์€ ๊ฒฐ). */
280
280
  const playbackRateRef = useRef(playbackRate);
@@ -285,7 +285,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
285
285
  * `load()` ๋Š” ๋ช…์„ธ๋Œ€๋กœ `playbackRate` ๋ฅผ **`defaultPlaybackRate` ๋กœ ๋˜๋Œ๋ฆฐ๋‹ค.** ๊ธฐ๋ณธ๊ฐ’์„
286
286
  * ์•ˆ ์„ธ์›Œ ๋‘๋ฉด ๊ทธ ๋˜๋Œ๋ฆผ์ด ๋Š˜ 1 ์ด๋ผ, ์†Œ๋ฆฌ๋ฅผ ์‹ค์„ ๋•Œ๋งˆ๋‹ค ์‚ฌ์šฉ์ž๊ฐ€ ๊ณ ๋ฅธ ๋น ๋ฅด๊ธฐ๊ฐ€ ์ง€์›Œ์ง„๋‹ค.
287
287
  */
288
- const ๋น ๋ฅด๊ธฐ๊ฑธ๊ธฐ = useCallback((a) => {
288
+ const applyPlaybackRate = useCallback((a) => {
289
289
  const r = playbackRateRef.current || 1.0;
290
290
  try {
291
291
  a.defaultPlaybackRate = r;
@@ -309,12 +309,12 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
309
309
  * ์ž๋ฆฌ๋ฅผ ํ•˜๋‚˜๋กœ ๋ชจ์€๋‹ค โ€” ํฉ์–ด์ง„ ๋‹ค์„ฏ์„ ๊ธ€๋กœ ๋‹จ์†ํ•˜์ง€ ์•Š๊ณ  **์…ˆ์œผ๋กœ ๋ง‰๋Š”๋‹ค**
310
310
  * (`test/audioPlaybackRateSurvivesLoad.test.tsx` ๊ฐ€ `load()` ์ž๋ฆฌ ์ˆ˜๋ฅผ ์„ผ๋‹ค).
311
311
  */
312
- const ์†Œ๋ฆฌ์‹ฃ๊ธฐ = useCallback((a, src) => {
312
+ const loadSound = useCallback((a, src) => {
313
313
  a.src = src;
314
- ๋น ๋ฅด๊ธฐ๊ฑธ๊ธฐ(a); // defaultPlaybackRate ๋ฅผ ๋จผ์ € ์„ธ์›Œ ๋‘”๋‹ค โ€” load() ๊ฐ€ ๊ทธ๊ฒƒ์œผ๋กœ ๋˜๋Œ๋ฆฐ๋‹ค
314
+ applyPlaybackRate(a); // defaultPlaybackRate ๋ฅผ ๋จผ์ € ์„ธ์›Œ ๋‘”๋‹ค โ€” load() ๊ฐ€ ๊ทธ๊ฒƒ์œผ๋กœ ๋˜๋Œ๋ฆฐ๋‹ค
315
315
  a.load();
316
- ๋น ๋ฅด๊ธฐ๊ฑธ๊ธฐ(a); // ๋˜๋Œ๋ฆฐ ๋’ค ํ•œ ๋ฒˆ ๋” โ€” ๊ธฐ๋ณธ๊ฐ’์„ ๋ชป ์„ธ์šฐ๋Š” ํ™˜๊ฒฝ์—์„œ๋„ ์„ ๋‹ค
317
- }, [๋น ๋ฅด๊ธฐ๊ฑธ๊ธฐ]);
316
+ applyPlaybackRate(a); // ๋˜๋Œ๋ฆฐ ๋’ค ํ•œ ๋ฒˆ ๋” โ€” ๊ธฐ๋ณธ๊ฐ’์„ ๋ชป ์„ธ์šฐ๋Š” ํ™˜๊ฒฝ์—์„œ๋„ ์„ ๋‹ค
317
+ }, [applyPlaybackRate]);
318
318
  /**
319
319
  * **์™„๋ฃŒ ์—ฐ์ถœ์„ ๊ฑท๋Š”๋‹ค** โ€” ์ƒˆ ์žฌ์ƒ์ด ์„œ๋ฉด ์ง€๋‚œ ์™„๋ฃŒ์˜ ์ž์ทจ๊ฐ€ ๊ทธ ์œ„์— ๋‚จ์•„์„  ์•ˆ ๋œ๋‹ค.
320
320
  *
@@ -332,17 +332,17 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
332
332
  * ๋ถ€๋ฅธ๋‹ค(๋‹ค์‹œ๋“ฃ๊ธฐ ยท โ–ถ ยท ๋ฒˆํ˜ธ ๋‹จ์ถ” ยท ๋ถˆ๋Ÿฌ์˜ค๊ธฐ๊ฐ€ ๋‹ค ๊ทธ ๊ธธ์ด๋‹ค). ๊ฑท๋Š” ๊ณณ์ด ์—ฌ๋Ÿฟ์ด๋ฉด
333
333
  * **ํ•œ ๊ณณ์ด ์•ˆ ๋”ฐ๋ผ๊ฐ„๋‹ค** โ€” ์˜ค๋Š˜ ๋ฐค ๋‚ด๋‚ด ๊ทธ ๊ฒฐ์ด์—ˆ๋‹ค.
334
334
  */
335
- const ์™„๋ฃŒ์—ฐ์ถœ๊ฑท๊ธฐ = useCallback(() => {
335
+ const clearDoneEffect = useCallback(() => {
336
336
  if (fillOutResetRef.current) {
337
337
  clearTimeout(fillOutResetRef.current);
338
338
  fillOutResetRef.current = null;
339
339
  }
340
340
  setFillCompletionPhase('idle');
341
341
  }, []);
342
- const tryPlayWithRetry = useCallback(async (a, attempts = 3, delayMs = 50, ์•ž๋œธํ๋ฅธ = 0) => {
342
+ const tryPlayWithRetry = useCallback(async (a, attempts = 3, delayMs = 50, leadElapsed = 0) => {
343
343
  /* ์ง€๋‚œ ์™„๋ฃŒ์˜ ์ ‘๋Š” ์—ฐ์ถœ์„ ๋จผ์ € ๊ฑท๋Š”๋‹ค โ€” ์œ„ ์„ค๋ช… ์ฐธ๊ณ  */
344
- ์™„๋ฃŒ์—ฐ์ถœ๊ฑท๊ธฐ();
345
- const ๋‚ด์„ธ๋Œ€ = ์ง„ํ–‰๊ธฐ.generation();
344
+ clearDoneEffect();
345
+ const myGeneration = sequencer.generation();
346
346
  /* **์ด์–ด ํŠธ๋Š” ๊ฒƒ์—๋Š” ์•ž๋œธ์„ ๋‹ค์‹œ ์•ˆ ์ค€๋‹ค**(์‚ฌ์šฉ์ž ์‹ ๊ณ , 2026-10-06).
347
347
  ์•ž๋œธ์€ *์†Œ๋ฆฌ ์•ž*์˜ ๊ตฌ๊ฐ„์ด๋‹ค. ์†Œ๋ฆฌ๊ฐ€ **์ด๋ฏธ ์‹œ์ž‘๋œ** ๊ฒƒ์„ ์ด์–ด ํŠธ๋Š”๋ฐ ์•ž๋œธ์„ ๋˜ ์ฃผ๋ฉด
348
348
  ์†Œ๋ฆฌ๋Š” ๋ฉˆ์ถ˜ ์ž๋ฆฌ(`currentTime`)์—์„œ ์ด์–ด ๊ฐ€๋Š”๋ฐ **์ง„ํ–‰์€ 0 ๋ถ€ํ„ฐ ๋‹ค์‹œ ์„ผ๋‹ค** โ€” ๋ง๊ท€
@@ -356,16 +356,16 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
356
356
  > ์†Œ๋ฆฌ ์ค‘์— ๋ฉˆ์ท„๋‹ค ์ด์„ ๋•Œ๋„ ๊ท€๋ฅผ ๋‹ค์‹œ ์ฑ„๋น„์‹œํ‚ค๋ ค **์•ž๋œธ์„ ์ฃผ๋Š”** ์ชฝ์ด ๋‚˜์„ ์ˆ˜๋„
357
357
  > ์žˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์‹œ๊ฐ„ํ‘œ๋ฅผ *์•ž๋œธ + ๋‚จ์€ ์†Œ๋ฆฌ + ๋’ค๋œธ* ์œผ๋กœ ๋‹ค์‹œ ๊ทธ๋ ค์•ผ ์–ด๊ธ‹๋‚˜์ง€ ์•Š๋Š”๋‹ค.
358
358
  > ๊ทธ๊ฒƒ์€ **์‚ฌ์žฅ๋‹˜๊ป˜ ์—ฌ์ญ ์ผ**์ด๋ผ ์ง€๊ธˆ์€ "์•ž๋œธ์€ ์†Œ๋ฆฌ ์•ž" ์ด๋ผ๋Š” ๋œป ๊ทธ๋Œ€๋กœ ๋‘”๋‹ค. */
359
- const ์ด์–ดํ‹€๊ธฐ = !!a && a.currentTime > 0 && !a.ended;
359
+ const resuming = !!a && a.currentTime > 0 && !a.ended;
360
360
  /* ์•ž๋œธ์—์„œ ๋ฉˆ์ท„๋‹ค ์ด์œผ๋ฉด **๋‚จ์€ ๋งŒํผ๋งŒ** ์ฑ„์šด๋‹ค โ€” ๋œธ๋„ ์‹œ๊ฐ„ํ‘œ์˜ ์ผ๋ถ€๋ผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ
361
361
  ์„ธ๋ฉด ๊ทธ ์ž๋ฆฌ์—์„œ ์ด์–ด์ง€์ง€ ์•Š๋Š”๋‹ค(์‚ฌ์šฉ์ž ๊ฒฐ์ •, 2026-10-06). */
362
- const ๋‚จ์€์•ž๋œธ = Math.max(0, leadInMsRef.current - Math.max(0, ์•ž๋œธํ๋ฅธ));
363
- if (๋‚จ์€์•ž๋œธ > 0 && !์ด์–ดํ‹€๊ธฐ) {
362
+ const leadRemaining = Math.max(0, leadInMsRef.current - Math.max(0, leadElapsed));
363
+ if (leadRemaining > 0 && !resuming) {
364
364
  /* **์Šคํ”ผ์ปค๋Š” ๋œธ์ด ์‹œ์ž‘๋  ๋•Œ ์žก๋Š”๋‹ค**((85) 2b, 2026-10-06). ์ „์—๋Š” `await a.play()` ๋ฐ”๋กœ
365
365
  ์•ž์—์„œ ์žก์•˜๋Š”๋ฐ, ๊ทธ๋Ÿฌ๋ฉด **์•ž๋œธ ๋‚ด๋‚ด ์ฃผ์ธ์ด ์—†๋‹ค** โ€” ๊ทธ ์‚ฌ์ด ๋‚จ์ด ์†Œ๋ฆฌ๋ฅผ ๋‚ด๋„ ์ด์ชฝ์„
366
366
  ๋ชป ๋ฉˆ์ถ”๊ณ , ๋œธ์ด ๋๋‚˜๋ฉด ๋‘˜์ด ํ•จ๊ป˜ ์šธ๋ฆฐ๋‹ค. ์†Œ์Œ์€ ์ด๋ฏธ ๋œธ๋ถ€ํ„ฐ ๊น”๋ฆฌ๋‹ˆ(0.268.0)
367
367
  **์†Œ๋ฆฌ๋ฅผ ๋‚ด๊ณ  ์žˆ๋Š” ๊ฒƒ์ด ๋งž๋‹ค.** */
368
- ์†Œ๋ฆฌ์žก๊ธฐ('audio');
368
+ acquireSound('audio');
369
369
  startTicker();
370
370
  /* ์•ž๋œธ๋ถ€ํ„ฐ **์žฌ์ƒ ์ค‘**์ด๋‹ค โ€” ์žฌ์ƒํŒ์ด ์ด์ œ ์•ž๋œธ๋ถ€ํ„ฐ ๋ณด์ด๋ฏ€๋กœ((86)) ๋‹จ์ถ”๋„ โธ ๋กœ ์„œ์•ผ
371
371
  ๋ˆ„๋ฅด๋Š” ๋œป๊ณผ ๋งž๋Š”๋‹ค. โ–ถ ๋กœ ๋‘๋ฉด "๋ฉˆ์ถ”๋ ค๊ณ  โ–ถ ๋ฅผ ๋ˆ„๋ฅด๋Š”" ๊ผด์ด ๋œ๋‹ค. */
@@ -383,24 +383,24 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
383
383
  }
384
384
  catch (e) { }
385
385
  }
386
- if (!(await ์ง„ํ–‰๊ธฐ.waitPad(๋‚จ์€์•ž๋œธ, 'lead', ์•ž๋œธํ๋ฅธ)))
386
+ if (!(await sequencer.waitPad(leadRemaining, 'lead', leadElapsed)))
387
387
  return false;
388
- if (!mountedRef.current || !์ง„ํ–‰๊ธฐ.isCurrent(๋‚ด์„ธ๋Œ€))
388
+ if (!mountedRef.current || !sequencer.isCurrent(myGeneration))
389
389
  return false;
390
390
  }
391
- ์ง„ํ–‰๊ธฐ.enterSound();
391
+ sequencer.enterSound();
392
392
  for (let i = 0; i < attempts; i++) {
393
- if (!mountedRef.current || !์ง„ํ–‰๊ธฐ.isCurrent(๋‚ด์„ธ๋Œ€))
393
+ if (!mountedRef.current || !sequencer.isCurrent(myGeneration))
394
394
  return false;
395
395
  try {
396
- ์†Œ๋ฆฌ์žก๊ธฐ('audio');
396
+ acquireSound('audio');
397
397
  await a.play();
398
398
  return true;
399
399
  }
400
400
  catch (err) {
401
401
  const errName = err instanceof Error ? err.name : (typeof err === 'object' && err && 'name' in err ? err.name : undefined);
402
402
  if (errName === 'AbortError') {
403
- if (!์ง„ํ–‰๊ธฐ.isCurrent(๋‚ด์„ธ๋Œ€))
403
+ if (!sequencer.isCurrent(myGeneration))
404
404
  return false;
405
405
  await new Promise((res) => setTimeout(res, delayMs));
406
406
  continue;
@@ -424,7 +424,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
424
424
  }
425
425
  catch (_) { }
426
426
  return false;
427
- }, [t, startTicker, ์ง„ํ–‰๊ธฐ, fadeInNoise, noiseSettingsRef, ์™„๋ฃŒ์—ฐ์ถœ๊ฑท๊ธฐ]);
427
+ }, [t, startTicker, sequencer, fadeInNoise, noiseSettingsRef, clearDoneEffect]);
428
428
  /**
429
429
  * **๋’ค๋œธ(์‚ฌ์ด ๋œธ)์—์„œ ๋ฉˆ์ท„๋‹ค ์ด์„ ๋•Œ** โ€” ๋‚จ์€ ๋งŒํผ ๋Œ๊ณ  ํ•˜๋˜ ์ผ์„ ์ž‡๋Š”๋‹ค.
430
430
  *
@@ -432,9 +432,9 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
432
432
  * ์งง์•„์ง€๊ณ  ์žฌ์ƒ๋ฐ”๊ฐ€ ๊ทธ ์ˆœ๊ฐ„ ํŠ„๋‹ค โ€” ์‚ฌ์šฉ์ž๊ฐ€ ์งš์€ ๊ฒƒ์ด ๊ทธ ์ž๋ฆฌ๋‹ค. `onEnded` ์˜ ์‚ฌ์Šฌ๊ณผ
433
433
  * **๊ฐ™์€ ๋ชจ์–‘**์ด์–ด์•ผ ํ•˜๋ฏ€๋กœ(์ง„ํ–‰๋ฐ” ์ผœ๊ธฐ ยท ์†Œ์Œ ยท ์„ธ๋Œ€ ๊ฐ€๋ฆผ ยท `leave()`) ์—ฌ๊ธฐ๋„ ๊ทธ๋Œ€๋กœ ๋‘”๋‹ค.
434
434
  */
435
- const ๋’ค๋œธ์ด์–ด๊ฐ€๊ธฐ = useCallback((ํ๋ฅธ) => {
436
- const ๋‚ด์„ธ๋Œ€ = ์ง„ํ–‰๊ธฐ.generation();
437
- const ๋‚จ์€ = Math.max(0, tailMsRef.current - Math.max(0, ํ๋ฅธ));
435
+ const continueAfterTail = useCallback((elapsed) => {
436
+ const myGeneration = sequencer.generation();
437
+ const remaining = Math.max(0, tailMsRef.current - Math.max(0, elapsed));
438
438
  startTicker();
439
439
  const ns = noiseSettingsRef.current;
440
440
  if (ns?.enabled && (ns.volume ?? 0) > 0) {
@@ -443,21 +443,21 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
443
443
  }
444
444
  catch (e) { }
445
445
  }
446
- const ์ž‡๊ธฐ = () => { try {
447
- ์ด์–ด๊ฐ€๊ธฐRefBox.current?.();
446
+ const resume = () => { try {
447
+ continueRef.current?.();
448
448
  }
449
449
  catch (e) { } };
450
- if (๋‚จ์€ <= 0) {
451
- ์ง„ํ–‰๊ธฐ.leave();
452
- ์ž‡๊ธฐ();
450
+ if (remaining <= 0) {
451
+ sequencer.leave();
452
+ resume();
453
453
  return;
454
454
  }
455
- ์ง„ํ–‰๊ธฐ.waitPad(๋‚จ์€, 'tail', ํ๋ฅธ).then((์ด์–ด๋„๋˜๋‚˜) => {
456
- ์ง„ํ–‰๊ธฐ.leave();
457
- if (์ด์–ด๋„๋˜๋‚˜ && ์ง„ํ–‰๊ธฐ.isCurrent(๋‚ด์„ธ๋Œ€))
458
- ์ž‡๊ธฐ();
455
+ sequencer.waitPad(remaining, 'tail', elapsed).then((mayContinue) => {
456
+ sequencer.leave();
457
+ if (mayContinue && sequencer.isCurrent(myGeneration))
458
+ resume();
459
459
  }).catch(() => { });
460
- }, [์ง„ํ–‰๊ธฐ, startTicker, fadeInNoise, noiseSettingsRef]);
460
+ }, [sequencer, startTicker, fadeInNoise, noiseSettingsRef]);
461
461
  const attachAudioListeners = useCallback(() => {
462
462
  const a = audioRef.current;
463
463
  if (!a)
@@ -465,7 +465,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
465
465
  const onPlay = () => {
466
466
  setPlaybackState("playing");
467
467
  setStateLocal("playMode");
468
- set๊ตฌ๊ฐ„ํ‘œ์‹œ(true);
468
+ setShowRange(true);
469
469
  startTicker();
470
470
  // ๋ฌธ์žฅ์„ Web Audio ๊ทธ๋ž˜ํ”„(gainยท๋ฏธํ„ฐ)์— ํƒœ์šด๋‹ค โ€” ์žฌ์ƒ ์ œ์Šค์ฒ˜ ์‹œ์ ์— resume, ์ปจํ…์ŠคํŠธ๊ฐ€ ๋‹ฌ๋ฆฌ๊ณ  ๋‚˜์„œ์•ผ
471
471
  // ํƒœ์šฐ๋ฏ€๋กœ ๊ทธ ์ „์—” ์š”์†Œ๊ฐ€ ์ œ ๋ณผ๋ฅจ์œผ๋กœ ์ง์ ‘ ์šธ๋ฆฐ๋‹ค(๋ฌด์Œ ๋ฐฉ์ง€). ํƒœ์šด ๋’ค ๋ณผ๋ฅจ์€ GainNode ๋กœ ๊ฑธ๋ฆฐ๋‹ค.
@@ -501,17 +501,17 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
501
501
  ๋‹ค์Œ ์†Œ๋ฆฌ๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ ์†Œ๋ฆฌ์˜ **์•ž ๋œธ**์ด ์ด์–ด ๋ถ™์–ด ์‚ฌ์ด๊ฐ€ ๋‘˜์„ ๋”ํ•œ ๋งŒํผ์ด ๋œ๋‹ค โ€”
502
502
  ๋ˆ„๋ฅผ ๋•Œ์˜ ๋œธ๊ณผ ์‚ฌ์ด์˜ ๋œธ์„ ๊ฐ€๋ฅด๋Š” ์ž๋ฆฌ๊ฐ€ ์—ฌ๊ธฐ๋‹ค. */
503
503
  const onEnded = () => {
504
- const ์ด์–ด๊ฐ€๊ธฐ = () => {
504
+ const continuePlay = () => {
505
505
  try {
506
506
  const sref = sourcesRef.current;
507
507
  const next = nextTrackIndex(sref, currentIndexRef.current);
508
508
  if (next !== null) {
509
509
  currentIndexRef.current = next;
510
510
  setCurrentIndex(next);
511
- set๊ตฌ๊ฐ„ํ‘œ์‹œ(true);
511
+ setShowRange(true);
512
512
  const nextSrc = sref?.[next] && sref[next].src;
513
513
  if (nextSrc && a) {
514
- ์†Œ๋ฆฌ์‹ฃ๊ธฐ(a, nextSrc);
514
+ loadSound(a, nextSrc);
515
515
  tryPlayWithRetry(a).catch(() => { });
516
516
  return;
517
517
  }
@@ -529,7 +529,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
529
529
  }
530
530
  catch (e) { }
531
531
  stopTicker();
532
- set๊ตฌ๊ฐ„ํ‘œ์‹œ(false);
532
+ setShowRange(false);
533
533
  // ๋งˆ์ง€๋ง‰ ํŠธ๋ž™ ์ข…๋ฃŒ ์‹œ ์†Œ์Œ ์ •์ง€
534
534
  fadeOutNoise();
535
535
  // Route through handleEnd (via ref) so stateLocal transitions to 'active' before onDone fires.
@@ -541,21 +541,21 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
541
541
  catch (e) { }
542
542
  };
543
543
  /* ๋ฉˆ์ท„๋‹ค ๋‹ค์‹œ ๋ˆ„๋ฅผ ๋•Œ **์—ฌ๊ธฐ์„œ๋ถ€ํ„ฐ** ์ž‡๋Š”๋‹ค โ€” ์ง„ํ–‰๊ธฐ๊ฐ€ ๊ทธ ์ž๋ฆฌ๋ฅผ ์•ˆ๋‹ค */
544
- ์ด์–ด๊ฐ€๊ธฐRefBox.current = ์ด์–ด๊ฐ€๊ธฐ;
544
+ continueRef.current = continuePlay;
545
545
  if (tailMsRef.current > 0) {
546
546
  /* ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋™์•ˆ ์ง„ํ–‰๋ฐ”๋ฅผ ๋Œ๋ฆฐ๋‹ค โ€” `pause` ๊ฐ€ ์ด๋ฏธ ์„ธ์› ์„ ์ˆ˜ ์žˆ์–ด ๋‹ค์‹œ ์ผ ๋‹ค.
547
547
  ๊ทธ ์‚ฌ์ด ๋ฉˆ์ถ”๊ฑฐ๋‚˜ ๋– ๋‚˜๋ฉด `waitPad` ๊ฐ€ ๊ฑฐ์ง“์„ ์ฃผ๊ณ  ์•„๋ฌด ์ผ๋„ ์•ˆ ํ•œ๋‹ค. */
548
548
  startTicker();
549
- ์ง„ํ–‰๊ธฐ.waitPad(tailMsRef.current, 'tail').then((์ด์–ด๋„๋˜๋‚˜) => {
549
+ sequencer.waitPad(tailMsRef.current, 'tail').then((mayContinue) => {
550
550
  /* ๋’ค๋œธ์ด ๋๋‚ฌ๋‹ค โ€” ์ž๋ฆฌ๋Š” ๋น„์šฐ๋˜ **์„ธ๋Œ€๋Š” ์•ˆ ์˜ฌ๋ฆฐ๋‹ค**(`abort` ๊ฐ€ ์•„๋‹ˆ๋‹ค). ์˜ฌ๋ฆฌ๋ฉด
551
551
  ๋ฐ”๋กœ ์•„๋ž˜์—์„œ ์ด์–ด ๊ฐˆ ์‚ฌ์Šฌ๊นŒ์ง€ ์ œ๊ฐ€ ๋ฒ„๋ฆฐ ๊ฒƒ์ด ๋œ๋‹ค. */
552
- ์ง„ํ–‰๊ธฐ.leave();
553
- if (์ด์–ด๋„๋˜๋‚˜)
554
- ์ด์–ด๊ฐ€๊ธฐ();
552
+ sequencer.leave();
553
+ if (mayContinue)
554
+ continuePlay();
555
555
  }).catch(() => { });
556
556
  return;
557
557
  }
558
- ์ด์–ด๊ฐ€๊ธฐ();
558
+ continuePlay();
559
559
  };
560
560
  a.addEventListener("play", onPlay);
561
561
  a.addEventListener("pause", onPause);
@@ -569,7 +569,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
569
569
  }
570
570
  catch (e) { }
571
571
  };
572
- }, [fadeInNoise, fadeOutNoise, tryPlayWithRetry, noiseSettingsRef, startTicker, stopTicker, ์ง„ํ–‰๊ธฐ]);
572
+ }, [fadeInNoise, fadeOutNoise, tryPlayWithRetry, noiseSettingsRef, startTicker, stopTicker, sequencer]);
573
573
  useEffect(() => {
574
574
  const cleanup = attachAudioListeners();
575
575
  return cleanup;
@@ -735,21 +735,21 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
735
735
  *
736
736
  * ์ฐจ๋ก€๋Š” ์ด๋ ‡๋‹ค: ๋Œ๋ฆผํŒ โ†’ ์ค€๋น„ ๋ โ†’ **์žฌ์ƒํŒยทํŒŒํ˜•ยท์žฌ์ƒ๋ฐ”๊ฐ€ ๋ณด์ž„** โ†’ ์•ž๋œธ(์†Œ์Œ) โ†’ ์†Œ๋ฆฌ.
737
737
  */
738
- const ์žฌ์ƒํŒ์ผœ๊ธฐ = () => {
738
+ const openPlayPanel = () => {
739
739
  setStateLocal('playMode');
740
740
  setCurrentIndex(0);
741
- set๊ตฌ๊ฐ„ํ‘œ์‹œ(true);
741
+ setShowRange(true);
742
742
  };
743
743
  const loadAndPlay = async (a, src) => {
744
744
  markCompletedByEnd(false);
745
745
  fetchingRef.current = false;
746
- ์†Œ๋ฆฌ์‹ฃ๊ธฐ(a, src);
746
+ loadSound(a, src);
747
747
  await waitForCanPlay(a);
748
748
  // ๊ธฐ๋‹ค๋ฆฌ๋Š” ์‚ฌ์ด ๋– ๋‚ฌ์œผ๋ฉด(false) ์ด์–ด ๊ฐ€์ง€ ์•Š๋Š”๋‹ค โ€” ๋–ผ์–ด์ง„ <audio> ๋ฅผ ํ‹€๋˜ ๊ฒƒ
749
749
  if ((await waitForWaveReady()) === false)
750
750
  return;
751
751
  setIsFetchingUrl(false);
752
- ์žฌ์ƒํŒ์ผœ๊ธฐ(); // ์•ž๋œธ์ด ์‹œ์ž‘๋˜๊ธฐ **์ „์—** โ€” ๋œธ ๋™์•ˆ ํŒ์ด ๋น„์ง€ ์•Š๊ฒŒ
752
+ openPlayPanel(); // ์•ž๋œธ์ด ์‹œ์ž‘๋˜๊ธฐ **์ „์—** โ€” ๋œธ ๋™์•ˆ ํŒ์ด ๋น„์ง€ ์•Š๊ฒŒ
753
753
  await tryPlayWithRetry(a);
754
754
  };
755
755
  const fetchAndPrepareFromTTSUrl = async (ttsUrl, playAfterLoad = false) => {
@@ -896,7 +896,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
896
896
  fetchingRef.current = false;
897
897
  if (first.src && a) {
898
898
  try {
899
- ์†Œ๋ฆฌ์‹ฃ๊ธฐ(a, first.src);
899
+ loadSound(a, first.src);
900
900
  // canplay ์ด๋ฒคํŠธ ๋Œ€๊ธฐ โ€” load() ์งํ›„ play() ํ˜ธ์ถœ ์‹œ ๋ฐœ์ƒํ•˜๋Š” AbortError ๋ฐฉ์ง€.
901
901
  // ์งง์€ ํŒŒ์ผ๋„ canplay๊ฐ€ ๋น ๋ฅด๊ฒŒ ๋ฐœ์ƒํ•˜๋ฏ€๋กœ ์žฌ์ƒ ์ง€์—ฐ ์—†์Œ.
902
902
  // (๊ฐ™์€ ๊ธฐ๋‹ค๋ฆผ์ด ์ด ํŒŒ์ผ ์œ„์ชฝ์— waitForCanPlay ๋กœ ์ด๋ฏธ ์žˆ์—ˆ๋‹ค. ์—ฌ๊ธฐ์— ํ•œ ๋ฒŒ
@@ -909,7 +909,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
909
909
  // ๋’ค๋Šฆ๊ฒŒ ๋‚˜ํƒ€๋‚˜๋Š” ๋นˆ ํ™”๋ฉด ๊ตฌ๊ฐ„๋„ ์ƒ๊ธฐ์ง€ ์•Š๋„๋ก ํ•œ๋‹ค.
910
910
  if (showSpinner)
911
911
  setIsFetchingUrl(false);
912
- ์žฌ์ƒํŒ์ผœ๊ธฐ(); // ์•ž๋œธ์ด ์‹œ์ž‘๋˜๊ธฐ **์ „์—** โ€” ๋œธ ๋™์•ˆ ํŒ์ด ๋น„์ง€ ์•Š๊ฒŒ
912
+ openPlayPanel(); // ์•ž๋œธ์ด ์‹œ์ž‘๋˜๊ธฐ **์ „์—** โ€” ๋œธ ๋™์•ˆ ํŒ์ด ๋น„์ง€ ์•Š๊ฒŒ
913
913
  if (a)
914
914
  await tryPlayWithRetry(a);
915
915
  }
@@ -924,7 +924,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
924
924
  }
925
925
  setStateLocal('playMode');
926
926
  setCurrentIndex(0);
927
- set๊ตฌ๊ฐ„ํ‘œ์‹œ(true);
927
+ setShowRange(true);
928
928
  }
929
929
  return finalSources.length > 0;
930
930
  }
@@ -944,7 +944,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
944
944
  * 300ms ์— โธ โ†’ ์†Œ์Œ `play()` ๊ฐ€ ๋˜ ๋ถˆ๋ฆฌ๊ณ  **๊ฐ™์€ ๋‹จ์–ด๊ฐ€ ๋‘ ๋ฒˆ ํ‹€์–ด์กŒ๋‹ค**(2622ms ยท 2945ms).
945
945
  * ๋‚ด ๋ชป์€ ์ €์žฅ์†Œ ์†Œ๋ฆฌ๋กœ๋งŒ ๋Œ๋ ค์„œ ํ†ต๊ณผํ–ˆ๋‹ค โ€” **๊ณ ์นœ ๋ฒŒ๋งŒ ์žฌ๊ณ  ์žˆ์—ˆ๋‹ค.**
946
946
  */
947
- const ์š”์†Œํ† ๊ธ€ = async (a) => {
947
+ const toggleElement = async (a) => {
948
948
  /* **๋œธ ์ค‘์ด๋ฉด ์žฌ์ƒ ์ค‘์ด๋‹ค**(๋ง๊ท€ ์š”์ฒญ (85) 0๋‹จ๊ณ„์—์„œ ์ฐพ์€ ๊ฒฐํ•จ, 2026-10-06).
949
949
  ์ „์—๋Š” `a.paused` ๋งŒ ๋ดค๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๋œธ ๋™์•ˆ ๋ฉ”์ธ ์š”์†Œ๋Š” **๋ฉˆ์ถฐ ์žˆ๋‹ค** โ€” ์•ž๋œธ์—๋Š” ์•„์ง
950
950
  `play()` ์ „์ด๊ณ  ์‚ฌ์ดยท๋’ค๋œธ์—๋Š” ์ด๋ฏธ `ended` ๋‹ค. ๊ทธ๋ž˜์„œ โธ ๋กœ ๋ณด์ด๋Š” ๋‹จ์ถ”๋ฅผ ๋œธ ์ค‘์—
@@ -953,8 +953,8 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
953
953
  +1000ms ์— ํ•œ ๋ฒˆ, ์›๋ž˜ ์‚ฌ์Šฌ์ด ๋‹ฟ๋Š” +1500ms ์— ๋˜ ํ•œ ๋ฒˆ).
954
954
  ๊ฐ์‹œ ํƒ€์ด๋จธ๊ฐ€ 0.268.0 ์—์„œ ๋ฐฐ์šด ๊ฒƒ๊ณผ ๊ฐ™์€ ๊ฒƒ์„ ์ด ๋‹จ์ถ”๊ฐ€ ๋ชป ๋ฐฐ์šฐ๊ณ  ์žˆ์—ˆ๋‹ค โ€”
955
955
  ๋œธ์€ ์†Œ๋ฆฌ๊ฐ€ ์•ˆ ๋‚˜๋Š” ๊ตฌ๊ฐ„์ด์ง€ **๋ฉˆ์ถ˜ ๊ตฌ๊ฐ„์ด ์•„๋‹ˆ๋‹ค.** */
956
- const ๋œธ๋„๋Š”์ค‘ = ์ง„ํ–‰๊ธฐ.isRunning();
957
- if (a.paused && !๋œธ๋„๋Š”์ค‘) {
956
+ const padRunning = sequencer.isRunning();
957
+ if (a.paused && !padRunning) {
958
958
  if (completedByEndRef.current) {
959
959
  await handleReplay();
960
960
  return;
@@ -964,26 +964,26 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
964
964
  ๋‹ค์‹œ ์„ธ์ง€ ์•Š๋Š”๋‹ค. */
965
965
  /* **๋ฉˆ์ถ˜ ์ž๋ฆฌ์—์„œ ์ด์–ด ๊ฐ„๋‹ค** โ€” ์–ด๋””์„œ ๋ช‡ ms ์˜€๋Š”์ง€๋Š” ์ง„ํ–‰๊ธฐ๊ฐ€ ์•ˆ๋‹ค.
966
966
  ๋œธ๋„ ์‹œ๊ฐ„ํ‘œ์˜ ์ผ๋ถ€๋ผ **๋‚จ์€ ๋งŒํผ๋งŒ** ์ฑ„์šด๋‹ค(์‚ฌ์šฉ์ž ๊ฒฐ์ •, 2026-10-06). */
967
- const ๋ฉˆ์ท„๋˜๊ณณ = ์ง„ํ–‰๊ธฐ.resume();
968
- if (๋ฉˆ์ท„๋˜๊ณณ) {
967
+ const resumedAt = sequencer.resume();
968
+ if (resumedAt) {
969
969
  setPlaybackState("playing");
970
- if (๋ฉˆ์ท„๋˜๊ณณ.phase === 'tail') {
971
- ๋’ค๋œธ์ด์–ด๊ฐ€๊ธฐ(๋ฉˆ์ท„๋˜๊ณณ.ํ๋ฅธ);
970
+ if (resumedAt.phase === 'tail') {
971
+ continueAfterTail(resumedAt.elapsed);
972
972
  return;
973
973
  }
974
- await tryPlayWithRetry(a, 3, 50, ๋ฉˆ์ท„๋˜๊ณณ.ํ๋ฅธ);
974
+ await tryPlayWithRetry(a, 3, 50, resumedAt.elapsed);
975
975
  return;
976
976
  }
977
977
  await tryPlayWithRetry(a);
978
978
  setPlaybackState("playing");
979
979
  }
980
980
  else {
981
- ์ง„ํ–‰๊ธฐ.pause();
982
- ๋ฉˆ์ถค์˜๋„();
981
+ sequencer.pause();
982
+ abortIntent();
983
983
  a.pause();
984
984
  setPlaybackState("paused");
985
985
  /* ๋œธ ์ค‘์—๋Š” `pause` ์‚ฌ๊ฑด์ด ์•ˆ ์˜จ๋‹ค(์š”์†Œ๊ฐ€ ์ด๋ฏธ ๋ฉˆ์ถฐ ์žˆ๋‹ค) โ€” ๊ทธ ์‚ฌ๊ฑด์ด ํ•˜๋˜ ์ผ์„ ์—ฌ๊ธฐ์„œ ํ•œ๋‹ค */
986
- if (๋œธ๋„๋Š”์ค‘) {
986
+ if (padRunning) {
987
987
  stopTicker();
988
988
  fadeOutNoise();
989
989
  }
@@ -1044,16 +1044,16 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
1044
1044
  if (stateLocal === 'playMode') {
1045
1045
  /* ์˜ค๋””์˜ค ํŒŒ์ผ๋กœ ๋ณ€ํ™˜๋œ ๊ฒฝ์šฐ audioRef๋กœ ์ œ์–ด. <audio> ๋Š” ๋Š˜ ์žˆ์œผ๋ฏ€๋กœ ๊ทธ๊ฒƒ๋งŒ ๋ณด๋ฉด ์Œ์„ฑ ํ•ฉ์„ฑ์œผ๋กœ ๋งํ•˜๋Š” ์ค‘(๋ฆฌ์กธ๋ฒ„ ์—†์Œยท์‹คํŒจ)์—๋„
1046
1046
  ์ด ๊ธธ๋กœ ๋“ค์–ด๊ฐ€ ์†Œ๋ฆฌ ์—†๋Š” <audio> ๋ฅผ ์žฌ์ƒํ•˜๋ ค ๋“ค๊ณ  ๋ง์€ ๋ฉˆ์ถ”์ง€ ์•Š์•˜๋‹ค โ€” ํ•ฉ์„ฑ๊ธฐ๊ฐ€ ๋งํ•˜๋Š” ์ค‘์ด๊ณ  <audio> ์— ์†Œ๋ฆฌ๊ฐ€ ์—†์œผ๋ฉด ํ•ฉ์„ฑ๊ธฐ๋ฅผ ๋ฉˆ์ถ˜๋‹ค. */
1047
- const ํ•ฉ์„ฑ์œผ๋กœ๋งํ•จ = !!synth && (synth.speaking || synth.paused) && !(a && (a.currentSrc || a.getAttribute('src')));
1048
- if (a && !ํ•ฉ์„ฑ์œผ๋กœ๋งํ•จ) {
1049
- await ์š”์†Œํ† ๊ธ€(a); // ์ €์žฅ์†Œ ์†Œ๋ฆฌ ๊ฐˆ๋ž˜์™€ **๊ฐ™์€ ํ•œ ๋ฒŒ**
1047
+ const speakingByTTS = !!synth && (synth.speaking || synth.paused) && !(a && (a.currentSrc || a.getAttribute('src')));
1048
+ if (a && !speakingByTTS) {
1049
+ await toggleElement(a); // ์ €์žฅ์†Œ ์†Œ๋ฆฌ ๊ฐˆ๋ž˜์™€ **๊ฐ™์€ ํ•œ ๋ฒŒ**
1050
1050
  return;
1051
1051
  }
1052
1052
  // speechSynthesis fallback
1053
1053
  try {
1054
1054
  if (synth && synth.paused) {
1055
1055
  try {
1056
- ์†Œ๋ฆฌ์žก๊ธฐ('tts');
1056
+ acquireSound('tts');
1057
1057
  synth.resume();
1058
1058
  setPlaybackState('playing');
1059
1059
  }
@@ -1079,30 +1079,30 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
1079
1079
  try {
1080
1080
  if (first.src && a) {
1081
1081
  try {
1082
- ์†Œ๋ฆฌ์‹ฃ๊ธฐ(a, first.src);
1083
- ์žฌ์ƒํŒ์ผœ๊ธฐ(); // ์•ž๋œธ์ด ์‹œ์ž‘๋˜๊ธฐ **์ „์—**
1082
+ loadSound(a, first.src);
1083
+ openPlayPanel(); // ์•ž๋œธ์ด ์‹œ์ž‘๋˜๊ธฐ **์ „์—**
1084
1084
  if (a)
1085
1085
  await tryPlayWithRetry(a);
1086
1086
  }
1087
1087
  catch (e) { }
1088
1088
  }
1089
- ์žฌ์ƒํŒ์ผœ๊ธฐ();
1089
+ openPlayPanel();
1090
1090
  return;
1091
1091
  }
1092
1092
  catch (e) { }
1093
1093
  }
1094
1094
  setStateLocal("load");
1095
1095
  // ์•ฑ์ด ์ค€ ๊ฒฝ๋กœ ๊ทœ์น™(resolvePath ๋“ฑ)์ด ๋˜์ ธ๋„ ๋ชป ์–ป์€ ๊ฒƒ๊ณผ ๊ฐ™์ด ๋‹ค๋ฃฌ๋‹ค โ€” ๊ฑฐ์ ˆ์ด ํ˜๋Ÿฌ 'load' ์— ๊ฐ‡ํ˜”๋‹ค
1096
- let ์–ป์Œ;
1096
+ let got;
1097
1097
  try {
1098
- ์–ป์Œ = await fetchAndPrepare(true);
1098
+ got = await fetchAndPrepare(true);
1099
1099
  }
1100
1100
  catch (e) {
1101
- ์–ป์Œ = false;
1101
+ got = false;
1102
1102
  }
1103
1103
  /* ์“ธ ์†Œ์Šค๋ฅผ ํ•˜๋‚˜๋„ ๋ชป ์–ป์œผ๋ฉด(๊ฒฝ๋กœ๋ฅผ ๋ชป ์ •ํ•œ ์ฝ”๋“œ๋ฟ ๋“ฑ) 'load' ์— ๊ฐ‡ํ˜€ ๋“ฃ๊ธฐ ๋‹จ์ถ”๊ฐ€ ์‚ฌ๋ผ์ง€๊ณ  ์•„๋ฌด ์กฐ์ž‘๋„
1104
1104
  ์•ˆ ๋๋‹ค โ€” ์•Œ๋ฆฌ๊ณ  ๋ˆ„๋ฅผ ์ˆ˜ ์žˆ๋Š” ์ƒํƒœ๋กœ ๋˜๋Œ๋ฆฐ๋‹ค. */
1105
- if (์–ป์Œ === false && mountedRef.current) {
1105
+ if (got === false && mountedRef.current) {
1106
1106
  try {
1107
1107
  setNoticeMessage(t('training.audioFetchError', '์‚ฌ์šด๋“œ ์†Œ์Šค๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.'));
1108
1108
  }
@@ -1116,7 +1116,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
1116
1116
  if (stateLocal === "playMode" || stateLocal === "active") {
1117
1117
  try {
1118
1118
  if (a)
1119
- await ์š”์†Œํ† ๊ธ€(a);
1119
+ await toggleElement(a);
1120
1120
  }
1121
1121
  catch (e) {
1122
1122
  const errName = e instanceof Error ? e.name : (typeof e === 'object' && e && 'name' in e ? e.name : undefined);
@@ -1153,8 +1153,8 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
1153
1153
  };
1154
1154
  const handleStop = () => {
1155
1155
  // ๊ธฐ๋‹ค๋ฆฌ๋˜ ํŠธ๋ž™ ์ด๋™ยท์žฌ์‹œ๋„๊ฐ€ ์ •์ง€ ๋’ค์— ์†Œ๋ฆฌ๋ฅผ ๋‚ด์ง€ ์•Š๊ฒŒ
1156
- ๋ฉˆ์ถค์˜๋„();
1157
- ์†Œ๋ฆฌ๋†“๊ธฐ();
1156
+ abortIntent();
1157
+ releaseSound();
1158
1158
  /* **`ttsMode` ๋ผ๊ณ  `<audio>` ๋ฅผ ์•ˆ ๋ฉˆ์ถ”๋ฉด ์•ˆ ๋œ๋‹ค**(2026-10-06). ์ „์—๋Š” ์—ฌ๊ธฐ์„œ `stopTTS()`
1159
1159
  ๋งŒ ํ•˜๊ณ  **์ผ์ฐ ๋Œ์•„๊ฐ”๋‹ค.** ๊ทธ๋Ÿฐ๋ฐ TTS ์—๋Š” ๊ธธ์ด ๋‘˜์ด๋‹ค โ€” ๋ฆฌ์กธ๋ฒ„๊ฐ€ ์žˆ์œผ๋ฉด **์ฃผ์†Œ๋ฅผ ๋ฐ›์•„
1160
1160
  `<audio>` ๋กœ ํŠผ๋‹ค**(๊ทธ ๊ธธ์— ๋œธยทํŒŒํ˜•ยท๋ฒˆํ˜ธ ๋‹จ์ถ”๊ฐ€ ๋‹ค ๋‹ฌ๋ ค ์žˆ๋‹ค). ๊ธฐ๊ธฐ ๋ชฉ์†Œ๋ฆฌ๋กœ ๋งํ•  ๋•Œ๋งŒ
@@ -1182,7 +1182,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
1182
1182
  setPlaybackState('stopped');
1183
1183
  setStateLocal('active');
1184
1184
  setProgress(0);
1185
- set๊ตฌ๊ฐ„ํ‘œ์‹œ(false);
1185
+ setShowRange(false);
1186
1186
  };
1187
1187
  /**
1188
1188
  * ํŠธ๋ž™์„ ๊ฐˆ์•„ ๋ผ์šฐ๊ณ  ์žฌ์ƒํ•œ๋‹ค โ€” ์ด์ „ยท๋‹ค์Œยท๋ฒˆํ˜ธ ๋‹จ์ถ”๊ฐ€ ๋ชจ๋‘ ์ด ํ•œ ๋ฒŒ์„ ์“ด๋‹ค.
@@ -1207,28 +1207,28 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
1207
1207
  `a.src` ๋ฅผ ๋‹ค์Œ ์†Œ๋ฆฌ๋กœ ๋ฐ”๊ฟ” ๋†“์•„์„œ๋‹ค.
1208
1208
  ๊ทธ ํ† ํฐ์€ **์„ธ๋Œ€์™€ ๊ฐ™์€ ์ผ**์ด์—ˆ๋‹ค((85) 2d) โ€” `๋ฉˆ์ถค์˜๋„()` ๊ฐ€ ๋‘˜ ๋‹ค ์˜ฌ๋ฆฌ๊ณ  ์—ฌ๊ธฐ์„œ
1209
1209
  ๋‘˜ ๋‹ค ์ง‘์—ˆ๋‹ค. ์„ธ๋Œ€ ํ•˜๋‚˜๋กœ ํ•ฉ์นœ๋‹ค: `abort()` ๋กœ ๊ฑท๊ณ , **๊ทธ ๋’ค์—** ์ œ ์„ธ๋Œ€๋ฅผ ์ง‘๋Š”๋‹ค. */
1210
- ๋ฉˆ์ถค์˜๋„();
1211
- const ๋‚ด์„ธ๋Œ€ = ์ง„ํ–‰๊ธฐ.generation();
1210
+ abortIntent();
1211
+ const myGeneration = sequencer.generation();
1212
1212
  const a = audioRef.current;
1213
1213
  const sref = sourcesRef.current;
1214
1214
  const src = sref?.[index]?.src;
1215
1215
  try {
1216
1216
  markCompletedByEnd(false);
1217
1217
  setCurrentIndex(index);
1218
- set๊ตฌ๊ฐ„ํ‘œ์‹œ(true); // ์žฌ์ƒ์ด ์‹œ์ž‘๋˜๊ธฐ ์ „์—๋„ ๋ฐํžŒ๋‹ค โ€” ์˜ˆ์ „์—” ํšจ๊ณผ๊ฐ€ ํ•ด ์ฃผ๋˜ ์ผ
1218
+ setShowRange(true); // ์žฌ์ƒ์ด ์‹œ์ž‘๋˜๊ธฐ ์ „์—๋„ ๋ฐํžŒ๋‹ค โ€” ์˜ˆ์ „์—” ํšจ๊ณผ๊ฐ€ ํ•ด ์ฃผ๋˜ ์ผ
1219
1219
  currentIndexRef.current = index;
1220
1220
  if (!src || !a)
1221
1221
  return;
1222
1222
  // ๊ฐ™์€ ๊ตฌ๊ฐ„์„ ๋‹ค์‹œ ํŠธ๋Š” ๊ฒƒ์ด๋ฉด ํŒŒํ˜•์€ ๊ทธ๋Œ€๋กœ๋‹ค โ€” ๊ดœํžˆ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.
1223
1223
  if (!sameSegment)
1224
1224
  markWaveNotReady();
1225
- ์†Œ๋ฆฌ์‹ฃ๊ธฐ(a, src);
1225
+ loadSound(a, src);
1226
1226
  if (!sameSegment && (await waitForWaveReady()) === false)
1227
1227
  return;
1228
- if (!์ง„ํ–‰๊ธฐ.isCurrent(๋‚ด์„ธ๋Œ€))
1228
+ if (!sequencer.isCurrent(myGeneration))
1229
1229
  return;
1230
1230
  await tryPlayWithRetry(a);
1231
- if (!์ง„ํ–‰๊ธฐ.isCurrent(๋‚ด์„ธ๋Œ€))
1231
+ if (!sequencer.isCurrent(myGeneration))
1232
1232
  return;
1233
1233
  setPlaybackState('playing');
1234
1234
  setStateLocal('playMode');
@@ -1269,8 +1269,8 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
1269
1269
  const holdDelay = 200;
1270
1270
  const outDelay = 340;
1271
1271
  markCompletedByEnd(true);
1272
- ๋ฉˆ์ถค์˜๋„();
1273
- ์†Œ๋ฆฌ๋†“๊ธฐ();
1272
+ abortIntent();
1273
+ releaseSound();
1274
1274
  try {
1275
1275
  setProgress(1);
1276
1276
  }
@@ -1305,7 +1305,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
1305
1305
  setPlaybackState('stopped');
1306
1306
  }
1307
1307
  catch (e) { }
1308
- set๊ตฌ๊ฐ„ํ‘œ์‹œ(false);
1308
+ setShowRange(false);
1309
1309
  if (disabledRef.current)
1310
1310
  return;
1311
1311
  try {
@@ -1360,10 +1360,10 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
1360
1360
  try {
1361
1361
  const a = audioRef.current;
1362
1362
  if (a && typeof a.playbackRate === 'number')
1363
- ๋น ๋ฅด๊ธฐ๊ฑธ๊ธฐ(a);
1363
+ applyPlaybackRate(a);
1364
1364
  }
1365
1365
  catch (e) { }
1366
- }, [playbackRate, ๋น ๋ฅด๊ธฐ๊ฑธ๊ธฐ]);
1366
+ }, [playbackRate, applyPlaybackRate]);
1367
1367
  // ๋ฌธ์žฅ(๋ฉ”์ธ) ๋ณผ๋ฅจ ๋ฐ˜์˜ โ€” ๋ฌธ์ œ ์†Œ๋ฆฌ ๋ณผ๋ฅจ(effectiveSpeechVolume = ์ „์ฒด ์†Œ๋ฆฌ). setElementVolume ์ด ๊ทธ๋ž˜ํ”„์— ํƒœ์šด
1368
1368
  // ์š”์†Œ๋ฉด GainNode, ์•„๋‹ˆ๋ฉด ์š”์†Œ volume ์— ๊ฑด๋‹ค(iOS ์‹ค๊ธฐ๊ธฐ๋Š” ์š”์†Œ volume ์ด ์•ˆ ๋จน๋Š”๋‹ค). ์žฌ์ƒ ์ค‘์ด๋ฉด ์†Œ์Œ๊ณผ ๊ฐ™์€ 150ms ๋žจํ”„๋กœ ์˜ฎ๊ธฐ๊ณ , ๋ฉˆ์ถฐ ์žˆ์œผ๋ฉด ๋ฐ”๋กœ ๊ฑด๋‹ค.
1369
1369
  // ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํƒญ์—์„œ๋Š” RAF ๊ฐ€ ๋ฉˆ์ถฐ ๋žจํ”„๊ฐ€ ์ค‘๊ฐ„์— ์„ค ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋๊ฐ’์„ ํƒ€์ด๋จธ๋กœ ํ•œ ๋ฒˆ ๋” ๋ฐ•๋Š”๋‹ค.
@@ -1374,7 +1374,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
1374
1374
  return;
1375
1375
  const target = Math.max(0, Math.min(1, speechVolume));
1376
1376
  const ramp = speechRampRef.current;
1377
- const ๋Š๊ธฐ = () => {
1377
+ const detach = () => {
1378
1378
  if (ramp.raf !== null) {
1379
1379
  cancelAnimationFrame(ramp.raf);
1380
1380
  ramp.raf = null;
@@ -1384,7 +1384,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
1384
1384
  ramp.timer = null;
1385
1385
  }
1386
1386
  };
1387
- ๋Š๊ธฐ();
1387
+ detach();
1388
1388
  const from = getElementVolume(a);
1389
1389
  if (a.paused || Math.abs(from - target) < 0.001 || typeof requestAnimationFrame !== 'function') {
1390
1390
  try {
@@ -1403,11 +1403,11 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
1403
1403
  ramp.raf = t < 1 ? requestAnimationFrame(step) : null;
1404
1404
  };
1405
1405
  ramp.raf = requestAnimationFrame(step);
1406
- ramp.timer = setTimeout(() => { ๋Š๊ธฐ(); try {
1406
+ ramp.timer = setTimeout(() => { detach(); try {
1407
1407
  setElementVolume(a, target);
1408
1408
  }
1409
1409
  catch (e) { } }, 200);
1410
- return ๋Š๊ธฐ;
1410
+ return detach;
1411
1411
  }, [speechVolume]);
1412
1412
  // fill (์ง„ํ–‰ ๋งˆ์ปค) ํด๋ž˜์Šค ๋ฐ ์ดˆ๊ธฐ ์œ„์น˜ ๊ณ„์‚ฐ โ€” RAF ๋ฃจํ”„์—์„œ .style.left๋ฅผ ์ง์ ‘ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค.
1413
1413
  const isFillHolding = fillCompletionPhase === 'hold';
@@ -1426,12 +1426,12 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
1426
1426
  ๋ˆ„๋ฅด๋ฉด ์†Œ๋ฆฌ๋ฅผ ๋ฐ›์œผ๋Ÿฌ ๊ฐ€๊ฑฐ๋‚˜ "๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค" ๋ฅผ ๋„์› ๋‹ค โ€” ๋งˆ์šฐ์Šค์™€ ๊ฐ™๊ฒŒ ํ‚ค๋ณด๋“œ๋„ ๋ง‰๋Š”๋‹ค */
1427
1427
  disabled: isDisabled,
1428
1428
  // ํ‚ค๋ณด๋“œ๋กœ ๋ˆŒ๋ €์„ ๋•Œ๋งŒ ์ดˆ์ ์„ ์˜ฎ๊ธด๋‹ค โ€” ๋งˆ์šฐ์Šค๋กœ ๋ˆ„๋ฅธ ์‚ฌ๋žŒ์—๊ฒŒ '์žฌ์ƒ / ๋ฉˆ์ถค' ์ดˆ์  ํˆดํŒ์ด ๋œจ์ง€ ์•Š๊ฒŒ
1429
- onClick: () => { ๋“ฃ๊ธฐ์ดˆ์ ์˜ฎ๊ธฐ๊ธฐRef.current = wasKeyboardPress(); hideListenTip(); handleToggle(); }, "aria-label": typeof label === 'string' ? label : t('training.listen', '๋“ฃ๊ธฐ'), onMouseEnter: showListenTip, onMouseLeave: hideListenTip,
1429
+ onClick: () => { moveFocusToListenRef.current = wasKeyboardPress(); hideListenTip(); handleToggle(); }, "aria-label": typeof label === 'string' ? label : t('training.listen', '๋“ฃ๊ธฐ'), onMouseEnter: showListenTip, onMouseLeave: hideListenTip,
1430
1430
  // ํ‚ค๋ณด๋“œ๋กœ ์ด ๋ฒ„ํŠผ์— ์˜ค๋ฉด ๋งˆ์šฐ์Šค์™€ ๋™์ผํ•˜๊ฒŒ ์•ˆ๋‚ด๋ฅผ ๋„์šด๋‹ค
1431
1431
  onFocus: showListenTip, onBlur: hideListenTip, children: _jsx("span", { ref: listenBtnRef, className: "audio-player__button-triangle-wrap", children: _jsx("svg", { className: "audio-player__button-triangle", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { ref: triPathRef, d: "M13.2 7.7 L16.5 9.85 Q20.2 12 16.5 14.15 L10 18.3 Q7 19.7 7 16.3 L7 7.7 Q7 4.3 10 5.7 L13.2 7.7" }) }) }) })) : null, listenBtnRect && (_jsx(InfoTooltip, { anchorRect: listenBtnRect, text: typeof label === 'string' && label ? label : t('training.listenLabel', '๋ฌธ์žฅ ๋“ฃ๊ธฐ'), offset: 8 })), _jsxs("div", { className: `audio-player__play ${stateLocal === "playMode" || displayedPlaying ? "is-visible" : ""}`, inert: !(stateLocal === "playMode" || displayedPlaying) || undefined, children: [_jsxs("div", { ref: btnsRef, className: "audio-player__buttons", children: [_jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "skip-back", onClick: handlePrev, ariaLabel: t('training.prev', '์ด์ „'), width: "60px", variant: "transparent-white", "aria-hidden": "true", tabIndex: -1, disabled: stateLocal !== 'playMode' }), _jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: displayedPlaying ? "pause" : "play", onClick: handleToggle, ariaLabel: t('common.playPause', '์žฌ์ƒ / ๋ฉˆ์ถค'), "aria-pressed": displayedPlaying, width: "60px", variant: "transparent-white", disabled: stateLocal !== 'playMode' }), _jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "square", onClick: handleStop, ariaLabel: t('training.stop', '์ •์ง€'), width: "60px", variant: "transparent-white", "aria-hidden": "true", tabIndex: -1, disabled: stateLocal !== 'playMode' }), sourceButtonCount > 1 ? (_jsx("div", { ref: sourceButtonsRef, className: "audio-player__source-buttons", children: Array.from({ length: sourceButtonCount }).map((_, idx) => (_jsx(Button, { htmlType: "button", size: 32, type: "dot", shape: "pill", useBorder: false, label: String(idx + 1), onClick: () => { try {
1432
1432
  handleJumpTo(idx);
1433
1433
  }
1434
- catch (e) { } }, ariaLabel: t('training.sourcePlay', { n: idx + 1 }), variant: "transparent-white", selected: ๊ตฌ๊ฐ„ํ‘œ์‹œ && currentIndex === idx, disabled: stateLocal !== 'playMode' || !(Array.isArray(sources) && sources[idx]), showTooltipInfo: true }, idx))) })) : null, (!Array.isArray(sources) || sources.length <= 1) ? (_jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "repeat", onClick: handleReplay, ariaLabel: t('common.replay', '๋‹ค์‹œ๋“ฃ๊ธฐ'), width: "60px", variant: "transparent-white", disabled: stateLocal !== 'playMode' })) : null, _jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "skip-forward", onClick: handleNext, ariaLabel: t('training.next', '๋‹ค์Œ'), width: "60px", variant: "transparent-white", "aria-hidden": true, tabIndex: -1, disabled: stateLocal !== 'playMode' }), _jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "check", onClick: handleEnd, ariaLabel: t('common.done', '์™„๋ฃŒ'), width: "60px", variant: "transparent-white", disabled: stateLocal !== 'playMode' })] }), _jsxs("div", { className: "audio-player__slider", children: [_jsx("div", { className: fillClass, ref: fillRef, style: { left: `${fillLeftPercent}%` }, "aria-hidden": "true" }), (showSoundWaveForm || showNoiseWaveForm) && (_jsx(WaveForm, { segments: currentSegments, progress: progress, totalDuration: totalDuration, waveProgressRef: waveProgressRef, showSoundWaveForm: showSoundWaveForm,
1434
+ catch (e) { } }, ariaLabel: t('training.sourcePlay', { n: idx + 1 }), variant: "transparent-white", selected: showRange && currentIndex === idx, disabled: stateLocal !== 'playMode' || !(Array.isArray(sources) && sources[idx]), showTooltipInfo: true }, idx))) })) : null, (!Array.isArray(sources) || sources.length <= 1) ? (_jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "repeat", onClick: handleReplay, ariaLabel: t('common.replay', '๋‹ค์‹œ๋“ฃ๊ธฐ'), width: "60px", variant: "transparent-white", disabled: stateLocal !== 'playMode' })) : null, _jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "skip-forward", onClick: handleNext, ariaLabel: t('training.next', '๋‹ค์Œ'), width: "60px", variant: "transparent-white", "aria-hidden": true, tabIndex: -1, disabled: stateLocal !== 'playMode' }), _jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "check", onClick: handleEnd, ariaLabel: t('common.done', '์™„๋ฃŒ'), width: "60px", variant: "transparent-white", disabled: stateLocal !== 'playMode' })] }), _jsxs("div", { className: "audio-player__slider", children: [_jsx("div", { className: fillClass, ref: fillRef, style: { left: `${fillLeftPercent}%` }, "aria-hidden": "true" }), (showSoundWaveForm || showNoiseWaveForm) && (_jsx(WaveForm, { segments: currentSegments, progress: progress, totalDuration: totalDuration, waveProgressRef: waveProgressRef, showSoundWaveForm: showSoundWaveForm,
1435
1435
  /* ๋œธ๋„ ํŒŒํ˜•์— ๊ทธ๋ฆฐ๋‹ค((74)) โ€” 2์ดˆ ๋‹จ์–ด์— ์•ž๋’ค 1์ดˆ๋ฉด 4์ดˆ๊ฐ€ ํ•œ ํญ์— ๋“ค์–ด๊ฐ„๋‹ค */
1436
1436
  leadInMs: leadInMs, tailMs: tailMs, trackDurationSec: sources?.[currentIndex]?.duration ?? 0, noiseSrc: (showNoiseWaveForm && noisePlayer?.settings.enabled && noiseVolume > 0) ? activeNoiseUrl : null, noiseRatio: noisePlayer ? noiseRatioOf(noisePlayer.settings) : 0, noiseSpeed: noisePlayer?.settings.speed ?? 1,
1437
1437
  /* ๊ทธ๋ฆผ์€ "๋ฌธ์žฅ = ๊ธฐ์ค€" โ€” ๋ฌธ์ œ ์†Œ๋ฆฌ(MรทR)๊ฐ€ ์•„๋‹ˆ๋ผ ์ „์ฒด ์†Œ๋ฆฌ M ์œผ๋กœ ๋ฌธ์žฅ์„ ๊ทธ๋ฆฌ๊ณ  ์†Œ์Œ์„ R ๋ฐฐ๋กœ ํ‚ค์šด๋‹ค */