@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
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { compareKorean } from '../../utils/textSort';
19
19
  /** 같은 시각 두 행의 차례 — 둘 다 id 가 있을 때만 가린다(숫자는 수 크기, 그 밖은 글자 사전순). 못 가리면 0. */
20
- function 같은시각차례(a, b) {
20
+ function sameTimeOrder(a, b) {
21
21
  if (a == null || b == null || a === '' || b === '')
22
22
  return 0;
23
23
  if (typeof a === 'number' && typeof b === 'number')
@@ -39,16 +39,16 @@ function 같은시각차례(a, b) {
39
39
  * (최신 먼저 넘기면 같은 시각 행의 최근이 뒤집힌다. 시각이 다른 행은 차례와 상관없이 맞다.)
40
40
  */
41
41
  export function summarizeMisses(records) {
42
- const 모음 = new Map();
42
+ const byKey = new Map();
43
43
  let counted = 0;
44
44
  let skippedNoTitle = 0;
45
45
  /* 최근 것을 알려면 시간순으로 훑어야 한다. 서버는 최신순으로 주지만 여기서 다시
46
46
  정렬한다 — 부르는 쪽의 차례에 기대면 그쪽이 바뀔 때 조용히 어긋난다. */
47
- const 시간순 = records
48
- .map((r, 자리) => ({ r, 자리 }))
49
- .sort((a, b) => a.r.answeredAt.localeCompare(b.r.answeredAt) || 같은시각차례(a.r.id, b.r.id) || a.자리 - b.자리)
47
+ const byTime = records
48
+ .map((r, at) => ({ r, at }))
49
+ .sort((a, b) => a.r.answeredAt.localeCompare(b.r.answeredAt) || sameTimeOrder(a.r.id, b.r.id) || a.at - b.at)
50
50
  .map(x => x.r);
51
- for (const r of 시간순) {
51
+ for (const r of byTime) {
52
52
  // 요약·시간 행은 문항이 아니다. 틀림으로 세면 한 판마다 오답이 늘어난다.
53
53
  if (r.isCorrect == null)
54
54
  continue;
@@ -60,7 +60,7 @@ export function summarizeMisses(records) {
60
60
  counted++;
61
61
  const id = r.itemId?.trim() || title;
62
62
  const key = `${r.trainingType}|${id}`;
63
- const cur = 모음.get(key) ?? {
63
+ const cur = byKey.get(key) ?? {
64
64
  trainingType: r.trainingType, itemId: id, title,
65
65
  tried: 0, wrong: 0,
66
66
  latestCorrect: r.isCorrect, latestAt: r.answeredAt, lastSelected: r.selected,
@@ -84,9 +84,9 @@ export function summarizeMisses(records) {
84
84
  if (at !== 0)
85
85
  cur.setIds.unshift(r.setId);
86
86
  }
87
- 모음.set(key, cur);
87
+ byKey.set(key, cur);
88
88
  }
89
- const items = [...모음.entries()].map(([key, v]) => ({
89
+ const items = [...byKey.entries()].map(([key, v]) => ({
90
90
  key,
91
91
  trainingType: v.trainingType,
92
92
  itemId: v.itemId,
@@ -126,13 +126,13 @@ export function nextMisses(items, limit = 8) {
126
126
  * 못 잡은 것이 많은 유형이 먼저. 같으면 유형 키로 갈라 차례를 못 박는다.
127
127
  */
128
128
  export function missesByType(items) {
129
- const 모음 = new Map();
129
+ const byKey = new Map();
130
130
  for (const i of items) {
131
- const arr = 모음.get(i.trainingType) ?? [];
131
+ const arr = byKey.get(i.trainingType) ?? [];
132
132
  arr.push(i);
133
- 모음.set(i.trainingType, arr);
133
+ byKey.set(i.trainingType, arr);
134
134
  }
135
- const out = [...모음.entries()].map(([trainingType, list]) => ({
135
+ const out = [...byKey.entries()].map(([trainingType, list]) => ({
136
136
  trainingType,
137
137
  items: list,
138
138
  pending: list.filter(i => !i.latestCorrect).length,
@@ -162,7 +162,7 @@ export const SENTENCE_TRAINING_TYPES = ['LISTEN_IN_NOISE', 'SHORT_STORY', 'LONG_
162
162
  * 밀렸다"고 해 놓고 두 개짜리 세트가 나온다 — 몇 개를 못 담았는지 함께 돌려준다.
163
163
  */
164
164
  export function reviewSetPlan(items) {
165
- const 모음 = new Map();
165
+ const byKey = new Map();
166
166
  let noSet = 0;
167
167
  for (const i of items) {
168
168
  if (i.latestCorrect)
@@ -173,17 +173,17 @@ export function reviewSetPlan(items) {
173
173
  continue;
174
174
  }
175
175
  const key = `${i.trainingType}|${setId}`;
176
- const cur = 모음.get(key) ?? { setId, trainingType: i.trainingType, pending: 0 };
176
+ const cur = byKey.get(key) ?? { setId, trainingType: i.trainingType, pending: 0 };
177
177
  cur.pending++;
178
- 모음.set(key, cur);
178
+ byKey.set(key, cur);
179
179
  }
180
180
  /* 못 잡은 것이 많은 세트가 먼저. 같으면 유형·uuid 로 갈라 차례를 못 박는다 —
181
181
  같은 자료에 두 번 물어 순서가 다르면 사람은 값이 바뀐 줄 안다. */
182
- const 정렬 = (a, b) => b.pending - a.pending || a.trainingType.localeCompare(b.trainingType) || a.setId.localeCompare(b.setId);
183
- const all = [...모음.values()];
182
+ const byOrder = (a, b) => b.pending - a.pending || a.trainingType.localeCompare(b.trainingType) || a.setId.localeCompare(b.setId);
183
+ const all = [...byKey.values()];
184
184
  return {
185
- sentence: all.filter(r => SENTENCE_TRAINING_TYPES.includes(r.trainingType)).sort(정렬),
186
- word: all.filter(r => !SENTENCE_TRAINING_TYPES.includes(r.trainingType)).sort(정렬),
185
+ sentence: all.filter(r => SENTENCE_TRAINING_TYPES.includes(r.trainingType)).sort(byOrder),
186
+ word: all.filter(r => !SENTENCE_TRAINING_TYPES.includes(r.trainingType)).sort(byOrder),
187
187
  noSet,
188
188
  };
189
189
  }
@@ -44,11 +44,11 @@ export function splitTrainingVariants(value) {
44
44
  }
45
45
  return [String(value)];
46
46
  }
47
- const 이름 = (key, 기본) => {
48
- if (!기본)
49
- return 기본;
50
- const s = typeof i18n?.t === "function" ? i18n.t(key, { defaultValue: 기본 }) : undefined;
51
- return typeof s === "string" && s ? s : 기본;
47
+ const labelOf = (key, fallback) => {
48
+ if (!fallback)
49
+ return fallback;
50
+ const s = typeof i18n?.t === "function" ? i18n.t(key, { defaultValue: fallback }) : undefined;
51
+ return typeof s === "string" && s ? s : fallback;
52
52
  };
53
53
  /** @public */
54
54
  export function resolveTrainingPopupMeta(trainingData, depth, fallbackTitle) {
@@ -71,8 +71,8 @@ export function resolveTrainingPopupMeta(trainingData, depth, fallbackTitle) {
71
71
  : undefined,
72
72
  // 카테고리 이름은 지금 언어로(topic.* · topic.sub.*, 사전에 없으면 등록된 이름)
73
73
  iconTooltip: [
74
- category ? 이름(`topic.${category.key}`, category.label) : undefined,
75
- subCategory ? 이름(`topic.sub.${subCategory.key}`, subCategory.label) : undefined,
74
+ category ? labelOf(`topic.${category.key}`, category.label) : undefined,
75
+ subCategory ? labelOf(`topic.sub.${subCategory.key}`, subCategory.label) : undefined,
76
76
  ].filter(Boolean).join(" / ") || undefined,
77
77
  };
78
78
  }
@@ -55,7 +55,7 @@ export function useAudioPreview(trainingData, typeKey) {
55
55
  const playingRef = useRef(false);
56
56
  /* 지금 미리 듣기를 멈추는 함수 — 스피커 자물쇠(currentSound.ts)에 건다. 다른 블록 미리 듣기·훈련 소리가 울리면 이것이 불린다.
57
57
  전에는 자물쇠에 참여하지 않아 바구니의 블록 둘을 잇달아 누르면 두 문장이 함께 울렸다. */
58
- const 멈춤Ref = useRef(null);
58
+ const stopRef = useRef(null);
59
59
  const previewBtnRef = useRef(null);
60
60
  const [previewHover, setPreviewHover] = useState(false);
61
61
  // downloadSound()로 만든 blob URL만 추적 — getSignedOrPublicUrl()이 반환하는 http(s) URL은
@@ -134,8 +134,8 @@ export function useAudioPreview(trainingData, typeKey) {
134
134
  e.stopPropagation();
135
135
  e.preventDefault();
136
136
  if (playingRef.current) {
137
- if (멈춤Ref.current)
138
- stopSoundIfOwner(멈춤Ref.current);
137
+ if (stopRef.current)
138
+ stopSoundIfOwner(stopRef.current);
139
139
  playingRef.current = false;
140
140
  try {
141
141
  previewAudioRef.current?.pause();
@@ -150,13 +150,13 @@ export function useAudioPreview(trainingData, typeKey) {
150
150
  return;
151
151
  /* pause() 만으로는 ended 가 오지 않아 기다리던 약속이 풀리지 않는다 — 멈출 때 풀어
152
152
  반복을 끝낸다. 그 일을 `playOneShot().stop()` 이 맡는다((85) 3단계로 한 자리에 모았다). */
153
- let 지금소리 = null;
154
- const 멈춤 = () => {
153
+ let currentSound = null;
154
+ const stop = () => {
155
155
  playingRef.current = false;
156
- 지금소리?.stop();
156
+ currentSound?.stop();
157
157
  };
158
- 멈춤Ref.current = 멈춤;
159
- const release = claimSound(멈춤);
158
+ stopRef.current = stop;
159
+ const release = claimSound(stop);
160
160
  playingRef.current = true;
161
161
  setPlaying(true);
162
162
  for (let i = 0; i < previewUrls.length; i++) {
@@ -166,16 +166,16 @@ export function useAudioPreview(trainingData, typeKey) {
166
166
  if (!url)
167
167
  continue;
168
168
  setPlayingIndex(i);
169
- 지금소리 = playOneShot(url);
170
- previewAudioRef.current = 지금소리.el;
169
+ currentSound = playOneShot(url);
170
+ previewAudioRef.current = currentSound.el;
171
171
  /* 끝까지 들었으면 다음 소리로. 못 틀었거나(error·거절) 멈췄으면 거기서 끝낸다 —
172
172
  전에는 끝까지 들음은 `resolve`, 못 틀음은 `reject`, 멈춤은 또 `resolve` 라 길이 셋이었다. */
173
- if (!(await 지금소리.done))
173
+ if (!(await currentSound.done))
174
174
  break;
175
175
  }
176
176
  release();
177
- if (멈춤Ref.current === 멈춤) {
178
- 멈춤Ref.current = null;
177
+ if (stopRef.current === stop) {
178
+ stopRef.current = null;
179
179
  playingRef.current = false;
180
180
  setPlaying(false);
181
181
  setPlayingIndex(-1);
@@ -184,8 +184,8 @@ export function useAudioPreview(trainingData, typeKey) {
184
184
  // Pause and release audio on unmount
185
185
  useEffect(() => {
186
186
  return () => {
187
- if (멈춤Ref.current)
188
- stopSoundIfOwner(멈춤Ref.current);
187
+ if (stopRef.current)
188
+ stopSoundIfOwner(stopRef.current);
189
189
  playingRef.current = false;
190
190
  try {
191
191
  previewAudioRef.current?.pause();
@@ -21,35 +21,35 @@ export function useFloatingFocus(open, findRoot, returnTo) {
21
21
  if (!open || typeof document === 'undefined')
22
22
  return;
23
23
  let raf = null;
24
- let 남은틀 = 10; // Tooltip 은 위치를 잰 뒤에야 보인다 — 보이기 전 focus() 는 먹지 않아 몇 틀 다시 해 본다
25
- const 넣기 = () => {
24
+ let framesLeft = 10; // Tooltip 은 위치를 잰 뒤에야 보인다 — 보이기 전 focus() 는 먹지 않아 몇 틀 다시 해 본다
25
+ const tryFocus = () => {
26
26
  raf = null;
27
27
  const root = findRootRef.current();
28
- const 첫칸 = root
28
+ const firstTarget = root
29
29
  ? Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).find((el) => !el.closest('[aria-hidden="true"]'))
30
30
  : undefined;
31
- if (첫칸) {
31
+ if (firstTarget) {
32
32
  try {
33
- 첫칸.focus();
33
+ firstTarget.focus();
34
34
  }
35
35
  catch (e) { /* 떼어진 노드 */ }
36
- if (document.activeElement === 첫칸)
36
+ if (document.activeElement === firstTarget)
37
37
  return;
38
38
  }
39
- if (--남은틀 > 0)
40
- raf = requestAnimationFrame(넣기);
39
+ if (--framesLeft > 0)
40
+ raf = requestAnimationFrame(tryFocus);
41
41
  };
42
- raf = requestAnimationFrame(넣기);
42
+ raf = requestAnimationFrame(tryFocus);
43
43
  return () => {
44
44
  if (raf !== null)
45
45
  cancelAnimationFrame(raf);
46
46
  // 닫혔다(또는 떠난다) — 초점이 창과 함께 사라져 body 로 떨어졌을 때만 연 단추로 돌려준다
47
- const 지금 = document.activeElement;
48
- if (!지금 || 지금 === document.body || !document.contains(지금) || !!지금.closest('.tooltip-content')) {
49
- const 단추 = returnToRef.current();
50
- if (단추) {
47
+ const active = document.activeElement;
48
+ if (!active || active === document.body || !document.contains(active) || !!active.closest('.tooltip-content')) {
49
+ const returnButton = returnToRef.current();
50
+ if (returnButton) {
51
51
  try {
52
- 단추.focus();
52
+ returnButton.focus();
53
53
  }
54
54
  catch (e) { /* 떼어진 노드 */ }
55
55
  }
@@ -38,7 +38,7 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, sequence
38
38
  /* **"지금 시간표가 도는가" 는 진행기에게 묻는다**((85) 2c, 2026-10-06). 전에는 자리를
39
39
  받아 **제 손으로 셈했다**(`p.phase !== 'sound' && p.at > 0`) — 같은 셈이 틱·토글·여기
40
40
  세 벌이었다. 셈이 한 자리에 있어야 "뜸이란 무엇인가" 가 갈라지지 않는다. */
41
- const 뜸중 = useCallback(() => !!sequencer?.isRunning(), [sequencer]);
41
+ const inPad = useCallback(() => !!sequencer?.isRunning(), [sequencer]);
42
42
  const cancelNoiseFade = useCallback(() => {
43
43
  if (noiseFadeRafRef.current !== null) {
44
44
  cancelAnimationFrame(noiseFadeRafRef.current);
@@ -215,7 +215,7 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, sequence
215
215
  `뜸중()` 만 지워도 시험 108 개가 다 통과한다. 둘이 같은 뜻이 아니라 **오늘 같이
216
216
  갈 뿐**이라 남겨 둔다. 지우려면 "뜸이면 playbackState 가 'playing' 이다" 를 먼저
217
217
  문으로 세워라 — 안 세우고 지우면 다음에 그 하나가 안 따라간다. */
218
- if (mainPlaying || 뜸중() || playbackStateRef.current === 'playing') {
218
+ if (mainPlaying || inPad() || playbackStateRef.current === 'playing') {
219
219
  misses = 0;
220
220
  return;
221
221
  }
@@ -226,7 +226,7 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, sequence
226
226
  }
227
227
  }, 400);
228
228
  return () => clearInterval(iv);
229
- }, [fadeOutNoise, noiseInUse, audioRef, playbackStateRef, 뜸중]);
229
+ }, [fadeOutNoise, noiseInUse, audioRef, playbackStateRef, inPad]);
230
230
  // 탭 백그라운드 전환 시 소음 즉시 정지 — 백그라운드 탭에서는 requestAnimationFrame이
231
231
  // 멈추거나 크게 스로틀돼 fadeOutNoise의 볼륨 램프가 완주하지 못할 수 있어, 페이드 없이
232
232
  // 바로 pause한다.
@@ -249,14 +249,14 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, sequence
249
249
  if (!noiseEl || !noiseEl.paused)
250
250
  return;
251
251
  const a = audioRef.current;
252
- const mainPlaying = (!!a && !a.paused && !a.ended) || 뜸중() || playbackStateRef.current === 'playing';
253
- const 지금 = noiseSettingsRef.current;
254
- if (mainPlaying && 지금?.enabled && 지금.volume > 0)
255
- fadeInNoise(지금.volume);
252
+ const mainPlaying = (!!a && !a.paused && !a.ended) || inPad() || playbackStateRef.current === 'playing';
253
+ const current = noiseSettingsRef.current;
254
+ if (mainPlaying && current?.enabled && current.volume > 0)
255
+ fadeInNoise(current.volume);
256
256
  };
257
257
  document.addEventListener('visibilitychange', handleVisibilityChange);
258
258
  return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
259
- }, [cancelNoiseFade, fadeInNoise, audioRef, playbackStateRef, 뜸중]);
259
+ }, [cancelNoiseFade, fadeInNoise, audioRef, playbackStateRef, inPad]);
260
260
  // noiseSettingsRef 최신화 (재생 이벤트 핸들러 클로저에서 사용)
261
261
  useEffect(() => {
262
262
  noiseSettingsRef.current = noisePlayer
@@ -264,8 +264,8 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, sequence
264
264
  : null;
265
265
  }, [noisePlayer, noisePlayer?.settings.enabled, noiseVolume, noisePlayer?.settings.type]);
266
266
  /* 새 소음 파일의 canplay 를 기다리는 핸들러 — 다음 종류 변경·떠날 때 뗀다(옛 파일 기다림이 새 설정 위에서 울리지 않게) */
267
- const canplay떼기Ref = useRef(null);
268
- useEffect(() => () => { canplay떼기Ref.current?.(); canplay떼기Ref.current = null; }, []);
267
+ const canplayOffRef = useRef(null);
268
+ useEffect(() => () => { canplayOffRef.current?.(); canplayOffRef.current = null; }, []);
269
269
  // 소음 설정 변경 → 오디오 반영
270
270
  useEffect(() => {
271
271
  const ns = noisePlayer?.settings;
@@ -292,24 +292,24 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, sequence
292
292
  noiseEl.src = url;
293
293
  noiseEl.load();
294
294
  const mainAudio = audioRef.current;
295
- if ((mainAudio && !mainAudio.paused) || 뜸중()) {
296
- canplay떼기Ref.current?.();
295
+ if ((mainAudio && !mainAudio.paused) || inPad()) {
296
+ canplayOffRef.current?.();
297
297
  const h = () => {
298
298
  noiseEl.removeEventListener('canplay', h);
299
- if (canplay떼기Ref.current === 떼기)
300
- canplay떼기Ref.current = null;
299
+ if (canplayOffRef.current === off)
300
+ canplayOffRef.current = null;
301
301
  // 파일 로드가 끝난 "이 시점"에 메인이 여전히 재생 중인지 재확인 —
302
302
  // 로드가 느리면 예약 시점과 실행 시점 사이에 메인이 끝났을 수 있다.
303
303
  /* 볼륨·켜짐은 **지금** 설정으로 — 효과 때 붙잡은 값으로 켜면, 불러오는 동안 치료사가 비율을 0.3 으로 내렸거나 소음을
304
304
  껐어도 canplay 에서 1 로 다시 켜져 끝까지 났다(난이도가 뜻보다 어려워지고 기록과 들린 소리가 어긋남). */
305
305
  const cur = audioRef.current;
306
- const 지금 = noiseSettingsRef.current;
307
- const 도나 = (!!cur && !cur.paused && !cur.ended) || 뜸중();
308
- if (도나 && 지금?.enabled && 지금.volume > 0)
309
- fadeInNoise(지금.volume);
306
+ const current = noiseSettingsRef.current;
307
+ const running = (!!cur && !cur.paused && !cur.ended) || inPad();
308
+ if (running && current?.enabled && current.volume > 0)
309
+ fadeInNoise(current.volume);
310
310
  };
311
- const 떼기 = () => noiseEl.removeEventListener('canplay', h);
312
- canplay떼기Ref.current = 떼기;
311
+ const off = () => noiseEl.removeEventListener('canplay', h);
312
+ canplayOffRef.current = off;
313
313
  noiseEl.addEventListener('canplay', h);
314
314
  }
315
315
  });
@@ -324,7 +324,7 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, sequence
324
324
  const mainAudio = audioRef.current;
325
325
  // speechSynthesis TTS 경로는 메인 <audio> 없이 재생된다 — 감시 타이머처럼 playbackState 도 본다(안 보면 비율만 바꿔도 소음이 멈췄다)
326
326
  // `뜸중()` 은 여기서도 겹옷이다 — 위 감시 타이머 옆 주석 참고.
327
- const mainPlaying = (!!mainAudio && !mainAudio.paused && !mainAudio.ended) || 뜸중() || playbackStateRef.current === 'playing';
327
+ const mainPlaying = (!!mainAudio && !mainAudio.paused && !mainAudio.ended) || inPad() || playbackStateRef.current === 'playing';
328
328
  if (mainPlaying && pendingNoiseUrlRef.current) {
329
329
  /* 종류를 바꾼 뒤 150ms 페이드아웃 안에 다른 설정을 바꾸면, 아래 볼륨만 옮기는 갈래가 파일을 바꿀 페이드를 지워 화면의 종류와
330
330
  들리는 종류가 문장 끝까지 달랐다 — 기다리는 파일이 있으면 fadeInNoise 가 그것을 끼운다. */
@@ -358,7 +358,7 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, sequence
358
358
  }
359
359
  }
360
360
  }
361
- }, [noisePlayer?.settings, noisePlayer?.settings.enabled, noiseVolume, noisePlayer?.settings.type, cancelNoiseFade, fadeInNoise, fadeOutNoise, audioRef, playbackStateRef, noiseSettingsRef, 뜸중]);
361
+ }, [noisePlayer?.settings, noisePlayer?.settings.enabled, noiseVolume, noisePlayer?.settings.type, cancelNoiseFade, fadeInNoise, fadeOutNoise, audioRef, playbackStateRef, noiseSettingsRef, inPad]);
362
362
  // 소음 재생 속도(playbackRate) 반영 — 0.75~1.25. 엘리먼트 속성이라 src 변경에도 유지됨
363
363
  useEffect(() => {
364
364
  const el = noiseAudioRef.current;
@@ -29,9 +29,9 @@ export function useObjectUrls() {
29
29
  }, []);
30
30
  /** 계속 쓸 것만 남기고 나머지는 되돌려준다 */
31
31
  const keepOnly = useCallback((keep) => {
32
- const 남길것 = new Set([...keep].filter(Boolean));
32
+ const keepSet = new Set([...keep].filter(Boolean));
33
33
  urlsRef.current = urlsRef.current.filter(u => {
34
- if (남길것.has(u))
34
+ if (keepSet.has(u))
35
35
  return true;
36
36
  try {
37
37
  URL.revokeObjectURL(u);
@@ -31,7 +31,7 @@ export interface SequencerPosition {
31
31
  /** 뜸에서 멈춘 자리 — **어느 뜸의 몇 ms 지점**이었나. 이을 때 남은 만큼만 채우는 데 쓴다 */
32
32
  export interface SequencerPause {
33
33
  phase: Exclude<PadPhase, 'sound'>;
34
- 흐른: number;
34
+ elapsed: number;
35
35
  }
36
36
  export interface PlaybackSequencer {
37
37
  /** 틱·소음·토글이 읽는다 — 매 틀 읽히므로 ref 다 */
@@ -85,7 +85,7 @@ export interface PlaybackSequencer {
85
85
  * 자리만 그만큼 **뒤로 밀어 둔다** — 그래야 재생바가 멈춘 값에서 이어진다. 안 밀면 남은
86
86
  * 시간만 기다리면서 바는 0 부터 다시 긴다(그 어긋남이 (90)·(91) 의 튐이었다).
87
87
  */
88
- waitPad: (ms: number, phase: Exclude<PadPhase, 'sound'>, 이미흐른?: number) => Promise<boolean>;
88
+ waitPad: (ms: number, phase: Exclude<PadPhase, 'sound'>, alreadyElapsed?: number) => Promise<boolean>;
89
89
  /** 소리 구간으로 들어간다 */
90
90
  enterSound: () => void;
91
91
  /**
@@ -2,8 +2,8 @@
2
2
  import { useCallback, useEffect, useMemo, useRef } from 'react';
3
3
  export function usePlaybackSequencer({ mountedRef }) {
4
4
  const positionRef = useRef({ phase: 'sound', at: 0 });
5
- const 세대Ref = useRef(0);
6
- const 뜸타이머Ref = useRef(null);
5
+ const generationRef = useRef(0);
6
+ const padTimerRef = useRef(null);
7
7
  /**
8
8
  * **그만둘 때 타이머를 걷지 않는다 — 세대로 가린다.**
9
9
  *
@@ -19,45 +19,45 @@ export function usePlaybackSequencer({ mountedRef }) {
19
19
  * 걷어 버리면 세대를 볼 일이 없다. 둘 중 하나를 골라야 하는 자리였다.)
20
20
  */
21
21
  /** 멈출 때의 자리 — 어느 뜸의 몇 ms 지점이었나. 뜸에서 안 멈췄으면 `null` */
22
- const 멈춘자리Ref = useRef(null);
22
+ const pausedAtRef = useRef(null);
23
23
  /* **`abort()` 보다 먼저 부른다** — `abort()` 는 자리를 비우므로 뒤에 부르면 "어느 구간에서
24
24
  멈췄나" 를 이미 잃는다. `pause()` 가 그 자리를 보고 적고, 그다음에 `abort()` 가 걷는다.
25
25
  (`abort()` 가 이 적바림까지 지우게 했다가 못이 울렸다 — 멈춘 자리를 바로 지워 버려
26
26
  이을 때 앞 소리가 또 났다. `멈춤의도()` 안에서 둘이 잇따라 불린다.) */
27
27
  const pause = useCallback(() => {
28
28
  const p = positionRef.current;
29
- const 뜸중 = p.phase !== 'sound' && p.at > 0;
29
+ const inPad = p.phase !== 'sound' && p.at > 0;
30
30
  /* **타이머는 안 걷는다** — 위 긴 설명 그대로다. 걷으면 약속이 안 풀려 뒷정리가 통째로
31
31
  안 돈다. 바로 뒤따르는 `abort()` 가 세대를 올리므로 돌던 뜸은 깨어나 거짓을 받고
32
32
  조용히 물러난다. 여기서는 **어디까지 갔었나**만 적는다. */
33
- 멈춘자리Ref.current = 뜸중
34
- ? { phase: p.phase, 흐른: Math.max(0, Date.now() - p.at) }
33
+ pausedAtRef.current = inPad
34
+ ? { phase: p.phase, elapsed: Math.max(0, Date.now() - p.at) }
35
35
  : null;
36
36
  }, []);
37
37
  const resume = useCallback(() => {
38
- const v = 멈춘자리Ref.current;
39
- 멈춘자리Ref.current = null;
38
+ const v = pausedAtRef.current;
39
+ pausedAtRef.current = null;
40
40
  return v;
41
41
  }, []);
42
42
  const abort = useCallback(() => {
43
- 세대Ref.current += 1;
43
+ generationRef.current += 1;
44
44
  positionRef.current = { phase: 'sound', at: 0 };
45
45
  }, []);
46
- const waitPad = useCallback((ms, phase, 이미흐른 = 0) => {
46
+ const waitPad = useCallback((ms, phase, alreadyElapsed = 0) => {
47
47
  if (!(ms > 0))
48
48
  return Promise.resolve(true);
49
- const 내세대 = 세대Ref.current;
49
+ const myGeneration = generationRef.current;
50
50
  /* 자리를 **이미 흐른 만큼 뒤로 밀어 둔다** — 재생바가 그 값에서 이어진다(위 설명) */
51
- positionRef.current = { phase, at: Date.now() - Math.max(0, 이미흐른) };
51
+ positionRef.current = { phase, at: Date.now() - Math.max(0, alreadyElapsed) };
52
52
  return new Promise((res) => {
53
- const 내타이머 = setTimeout(() => {
54
- if (뜸타이머Ref.current === 내타이머)
55
- 뜸타이머Ref.current = null;
53
+ const myTimer = setTimeout(() => {
54
+ if (padTimerRef.current === myTimer)
55
+ padTimerRef.current = null;
56
56
  /* **깨어날 때 제 세대인지 본다.** 앞 기다림을 안 걷으므로 둘이 함께 깨어날 수 있는데,
57
57
  그때 낡은 쪽은 거짓을 받고 조용히 물러난다 — 사슬이 겹치지 않는 자리가 여기다. */
58
- res(!!mountedRef.current && 내세대 === 세대Ref.current);
58
+ res(!!mountedRef.current && myGeneration === generationRef.current);
59
59
  }, ms);
60
- 뜸타이머Ref.current = 내타이머;
60
+ padTimerRef.current = myTimer;
61
61
  });
62
62
  }, [mountedRef]);
63
63
  const enterSound = useCallback(() => { positionRef.current = { phase: 'sound', at: Date.now() }; }, []);
@@ -68,14 +68,14 @@ export function usePlaybackSequencer({ mountedRef }) {
68
68
  const p = positionRef.current;
69
69
  return p.phase !== 'sound' && p.at > 0;
70
70
  }, []);
71
- const generation = useCallback(() => 세대Ref.current, []);
72
- const isCurrent = useCallback((gen) => gen === 세대Ref.current, []);
73
- const __bumpGenerationForTest = useCallback(() => { 세대Ref.current += 1; }, []);
71
+ const generation = useCallback(() => generationRef.current, []);
72
+ const isCurrent = useCallback((gen) => gen === generationRef.current, []);
73
+ const __bumpGenerationForTest = useCallback(() => { generationRef.current += 1; }, []);
74
74
  /* 화면을 떠나면 반드시 걷는다 — 마지막 그물 */
75
75
  useEffect(() => () => {
76
- if (뜸타이머Ref.current) {
77
- clearTimeout(뜸타이머Ref.current);
78
- 뜸타이머Ref.current = null;
76
+ if (padTimerRef.current) {
77
+ clearTimeout(padTimerRef.current);
78
+ padTimerRef.current = null;
79
79
  }
80
80
  }, []);
81
81
  /**