@ihabdevteam/core 0.286.0 → 0.289.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.
- package/CHANGELOG.md +90 -0
- package/dist/components.cjs.js +12 -12
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +16 -16
- package/dist/ihabdevteam-core.d.ts +1 -1
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +17 -23
- package/dist/packages/core/src/components/Training/audioTimeline.d.ts +17 -1
- package/dist/packages/core/src/components/Training/audioTimeline.js +18 -2
- package/dist/packages/core/src/components/Training/useNoiseFade.js +6 -0
- package/dist/packages/core/src/components/Training/useProgressTicker.js +17 -4
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +1 -1
- package/package.json +1 -1
|
@@ -1651,7 +1651,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1651
1651
|
*
|
|
1652
1652
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1653
1653
|
*/
|
|
1654
|
-
export declare const CORE_VERSION = "0.
|
|
1654
|
+
export declare const CORE_VERSION = "0.289.0";
|
|
1655
1655
|
|
|
1656
1656
|
/** @public */
|
|
1657
1657
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -50,10 +50,6 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
50
50
|
const objectUrls = useObjectUrls();
|
|
51
51
|
const sourceButtonsRef = useRef(null);
|
|
52
52
|
const fetchingRef = useRef(false);
|
|
53
|
-
// handlePrev/handleNext/handleJumpTo 세대 토큰 — 빠른 연타로 여러 호출이 겹치면
|
|
54
|
-
// src 교체/load()가 서로 인터리빙되며 "선택된" UI가 실제 재생 중인 트랙과 어긋날 수 있다.
|
|
55
|
-
// 각 호출 시작 시 증가시키고, await 이후 최신 호출인지 확인해 아니면 중단한다.
|
|
56
|
-
const jumpTokenRef = useRef(0);
|
|
57
53
|
const triPathRef = useRef(null);
|
|
58
54
|
/* 듣기 단추 설명 툴팁이 붙을 자리 — 가리키기와 초점 둘 다 받고, 다른 곳을 누르면 닫는다. */
|
|
59
55
|
/* 구조 분해해서 받는다 — `listenBtnRect` 처럼 점을 찍어 쓰면 lint 규칙이 그 객체를
|
|
@@ -151,8 +147,6 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
151
147
|
const tailMsRef = useRef(tailMs);
|
|
152
148
|
useEffect(() => { leadInMsRef.current = Math.max(0, leadInMs || 0); }, [leadInMs]);
|
|
153
149
|
useEffect(() => { tailMsRef.current = Math.max(0, tailMs || 0); }, [tailMs]);
|
|
154
|
-
/** 뜸만큼 기다린다 — 그 사이 **의도가 바뀌면 거짓**을 돌려 부르는 쪽이 그만둔다 */
|
|
155
|
-
const 뜸기다리기 = 진행기.waitPad;
|
|
156
150
|
const { start: startTicker, stop: stopTicker, settleTo: settleTicker, } = useProgressTicker({ audioRef, fillRef, waveProgressRef, sourcesRef, currentIndexRef, sequencer: 진행기, leadInMs, tailMs });
|
|
157
151
|
/* 파형이 자리를 잡을 때까지 기다리는 규칙은 useWaveReady 가 들고 있다 —
|
|
158
152
|
ref 둘·함수 둘에 "준비 안 됨으로 되돌리기"가 일곱 군데 흩어져 있던 것을 모았다. */
|
|
@@ -234,7 +228,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
234
228
|
*
|
|
235
229
|
* 이제 세대 하나다. `멈춤의도()` 는 세대를 올리고 자리를 비운다.
|
|
236
230
|
*/
|
|
237
|
-
const 멈춤의도 = () => { 진행기.abort();
|
|
231
|
+
const 멈춤의도 = () => { 진행기.abort(); };
|
|
238
232
|
/* 스피커는 하나(currentSound.ts) — 한 화면의 다른 재생기(문장 듣기·문제 듣기)·낱말 카드·미리 듣기가 울리면 이 재생기는
|
|
239
233
|
멈춘다. 전에는 낱말 카드만 자물쇠를 잡아, 듣기 단계가 둘인 화면·듣기+카드 단계에서 두 소리가 함께 울렸다.
|
|
240
234
|
멈춤 단추와 같게 멈춘다(이어 듣기 가능). 함수는 한 벌이어야 자물쇠가 '내 것' 을 알아본다. */
|
|
@@ -348,7 +342,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
348
342
|
}
|
|
349
343
|
catch (e) { }
|
|
350
344
|
}
|
|
351
|
-
if (!(await
|
|
345
|
+
if (!(await 진행기.waitPad(leadInMsRef.current, 'lead')))
|
|
352
346
|
return false;
|
|
353
347
|
if (!mountedRef.current || !진행기.isCurrent(내세대))
|
|
354
348
|
return false;
|
|
@@ -389,7 +383,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
389
383
|
}
|
|
390
384
|
catch (_) { }
|
|
391
385
|
return false;
|
|
392
|
-
}, [t, startTicker,
|
|
386
|
+
}, [t, startTicker, 진행기, fadeInNoise, noiseSettingsRef]);
|
|
393
387
|
const attachAudioListeners = useCallback(() => {
|
|
394
388
|
const a = audioRef.current;
|
|
395
389
|
if (!a)
|
|
@@ -476,9 +470,9 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
476
470
|
이어가기RefBox.current = 이어가기;
|
|
477
471
|
if (tailMsRef.current > 0) {
|
|
478
472
|
/* 기다리는 동안 진행바를 돌린다 — `pause` 가 이미 세웠을 수 있어 다시 켠다.
|
|
479
|
-
그 사이 멈추거나 떠나면
|
|
473
|
+
그 사이 멈추거나 떠나면 `waitPad` 가 거짓을 주고 아무 일도 안 한다. */
|
|
480
474
|
startTicker();
|
|
481
|
-
|
|
475
|
+
진행기.waitPad(tailMsRef.current, 'tail').then((이어도되나) => {
|
|
482
476
|
/* 뒤뜸이 끝났다 — 자리는 비우되 **세대는 안 올린다**(`abort` 가 아니다). 올리면
|
|
483
477
|
바로 아래에서 이어 갈 사슬까지 제가 버린 것이 된다. */
|
|
484
478
|
진행기.leave();
|
|
@@ -501,7 +495,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
501
495
|
}
|
|
502
496
|
catch (e) { }
|
|
503
497
|
};
|
|
504
|
-
}, [fadeInNoise, fadeOutNoise, tryPlayWithRetry, noiseSettingsRef, startTicker, stopTicker,
|
|
498
|
+
}, [fadeInNoise, fadeOutNoise, tryPlayWithRetry, noiseSettingsRef, startTicker, stopTicker, 진행기]);
|
|
505
499
|
useEffect(() => {
|
|
506
500
|
const cleanup = attachAudioListeners();
|
|
507
501
|
return cleanup;
|
|
@@ -1127,16 +1121,16 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
1127
1121
|
* 이동 토큰은 연타 방지다 — 파형을 기다리는 사이 더 최신 이동이 오면 이 호출은 물러선다.
|
|
1128
1122
|
*/
|
|
1129
1123
|
const goToTrack = async (index, { sameSegment = false } = {}) => {
|
|
1130
|
-
/* **먼저 돌고 있는 시간표를 걷는다**(2026-10-06).
|
|
1131
|
-
|
|
1132
|
-
사슬이 둘이 된다 — 가짜 시계 실측(소리 둘 · 뜸 1000 · 사이
|
|
1133
|
-
`b.mp3` 가 3100ms 와 3600ms 에 **두 번** 났다. 게다가
|
|
1134
|
-
났다** — 이쪽이 뜸을 기다리는 동안 살아 있던 사슬이
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1124
|
+
/* **먼저 돌고 있는 시간표를 걷는다**(2026-10-06). 전에는 트랙 이동이 **제 토큰**
|
|
1125
|
+
(`jumpTokenRef`)으로 이동끼리만 가렸고 **뜸 사슬은 그대로 살아 있었다.** 그래서 사이
|
|
1126
|
+
뜸에 번호 단추를 누르면 사슬이 둘이 된다 — 가짜 시계 실측(소리 둘 · 뜸 1000 · 사이
|
|
1127
|
+
뜸 2100ms 에 [소스 1]): `b.mp3` 가 3100ms 와 3600ms 에 **두 번** 났다. 게다가
|
|
1128
|
+
**누른 것은 1번인데 2번이 났다** — 이쪽이 뜸을 기다리는 동안 살아 있던 사슬이
|
|
1129
|
+
`a.src` 를 다음 소리로 바꿔 놓아서다.
|
|
1130
|
+
그 토큰은 **세대와 같은 일**이었다((85) 2d) — `멈춤의도()` 가 둘 다 올리고 여기서
|
|
1131
|
+
둘 다 집었다. 세대 하나로 합친다: `abort()` 로 걷고, **그 뒤에** 제 세대를 집는다. */
|
|
1138
1132
|
멈춤의도();
|
|
1139
|
-
const
|
|
1133
|
+
const 내세대 = 진행기.generation();
|
|
1140
1134
|
const a = audioRef.current;
|
|
1141
1135
|
const sref = sourcesRef.current;
|
|
1142
1136
|
const src = sref?.[index]?.src;
|
|
@@ -1153,10 +1147,10 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
1153
1147
|
소리싣기(a, src);
|
|
1154
1148
|
if (!sameSegment && (await waitForWaveReady()) === false)
|
|
1155
1149
|
return;
|
|
1156
|
-
if (
|
|
1150
|
+
if (!진행기.isCurrent(내세대))
|
|
1157
1151
|
return;
|
|
1158
1152
|
await tryPlayWithRetry(a);
|
|
1159
|
-
if (
|
|
1153
|
+
if (!진행기.isCurrent(내세대))
|
|
1160
1154
|
return;
|
|
1161
1155
|
setPlaybackState('playing');
|
|
1162
1156
|
setStateLocal('playMode');
|
|
@@ -54,7 +54,23 @@ export declare function trackSegments(tl: Timeline, track: number): {
|
|
|
54
54
|
* **이 트랙 안에서 얼마나 왔나**(0~1).
|
|
55
55
|
*
|
|
56
56
|
* `elapsedMs` 는 **그 구간 안에서** 흐른 시간이다 — 소리 구간이면 `currentTime × 1000`.
|
|
57
|
-
*
|
|
57
|
+
*
|
|
58
|
+
* ### 소리 길이를 몰라도(0) 뜸이 있으면 **뜸으로 잰다**(2026-10-06)
|
|
59
|
+
*
|
|
60
|
+
* 전에는 길이를 모르면 **무조건 0** 을 줬다. 까닭은 *"모르는 것을 100% 로 그리면 마커가
|
|
61
|
+
* 끝에 붙어 있다 튄다"* 였는데, 흐른 시간을 **그 구간 길이로 자르므로**(아래 `흐른`)
|
|
62
|
+
* 앞뜸에서는 아무리 흘러도 `앞뜸/(앞뜸+뒤뜸)` 을 못 넘는다 — 끝에 붙지 않는다.
|
|
63
|
+
*
|
|
64
|
+
* 그 0 이 실제로 한 일은 **앞뜸 1초 동안 재생바를 죽이는 것**이었다. 말귀가 0.288.0 을
|
|
65
|
+
* 앱에서 재니 앞뜸 내내 `0.0%` 였다가 소리가 나는 순간 `38.3%` 로 튀었다 — 길이 재기가
|
|
66
|
+
* 0 을 줘서 여기로 들어온 것이다. 사용자가 (86) 에서 직접 짚은 자리다.
|
|
67
|
+
*
|
|
68
|
+
* 이제는 **소리를 0 으로 둔 시간표**로 잰다(앞뜸+뒤뜸이 폭을 다 쓴다). 길이를 알게 되면
|
|
69
|
+
* 그때 제 시간표로 바뀌므로 그 순간 값이 **뒤로 물러난다** — 어림이 늘 참보다 크기
|
|
70
|
+
* 때문이다(소리가 길수록 뜸의 몫이 준다). 죽은 바보다 낫다는 쪽을 택했다.
|
|
71
|
+
*
|
|
72
|
+
* > 파형(`timelineBarSpans`)은 아직 길이를 모르면 뜸을 안 그린다. 둘이 어긋나는 자리다 —
|
|
73
|
+
* > 뜸을 쓰는 훈련 갈래는 파형을 끄고 재생바만 보이는 쪽이 많아 먼저 바를 살렸다.
|
|
58
74
|
*/
|
|
59
75
|
export declare function timelineProgress(tl: Timeline, at: {
|
|
60
76
|
track: number;
|
|
@@ -26,11 +26,27 @@ export function trackSegments(tl, track) {
|
|
|
26
26
|
* **이 트랙 안에서 얼마나 왔나**(0~1).
|
|
27
27
|
*
|
|
28
28
|
* `elapsedMs` 는 **그 구간 안에서** 흐른 시간이다 — 소리 구간이면 `currentTime × 1000`.
|
|
29
|
-
*
|
|
29
|
+
*
|
|
30
|
+
* ### 소리 길이를 몰라도(0) 뜸이 있으면 **뜸으로 잰다**(2026-10-06)
|
|
31
|
+
*
|
|
32
|
+
* 전에는 길이를 모르면 **무조건 0** 을 줬다. 까닭은 *"모르는 것을 100% 로 그리면 마커가
|
|
33
|
+
* 끝에 붙어 있다 튄다"* 였는데, 흐른 시간을 **그 구간 길이로 자르므로**(아래 `흐른`)
|
|
34
|
+
* 앞뜸에서는 아무리 흘러도 `앞뜸/(앞뜸+뒤뜸)` 을 못 넘는다 — 끝에 붙지 않는다.
|
|
35
|
+
*
|
|
36
|
+
* 그 0 이 실제로 한 일은 **앞뜸 1초 동안 재생바를 죽이는 것**이었다. 말귀가 0.288.0 을
|
|
37
|
+
* 앱에서 재니 앞뜸 내내 `0.0%` 였다가 소리가 나는 순간 `38.3%` 로 튀었다 — 길이 재기가
|
|
38
|
+
* 0 을 줘서 여기로 들어온 것이다. 사용자가 (86) 에서 직접 짚은 자리다.
|
|
39
|
+
*
|
|
40
|
+
* 이제는 **소리를 0 으로 둔 시간표**로 잰다(앞뜸+뒤뜸이 폭을 다 쓴다). 길이를 알게 되면
|
|
41
|
+
* 그때 제 시간표로 바뀌므로 그 순간 값이 **뒤로 물러난다** — 어림이 늘 참보다 크기
|
|
42
|
+
* 때문이다(소리가 길수록 뜸의 몫이 준다). 죽은 바보다 낫다는 쪽을 택했다.
|
|
43
|
+
*
|
|
44
|
+
* > 파형(`timelineBarSpans`)은 아직 길이를 모르면 뜸을 안 그린다. 둘이 어긋나는 자리다 —
|
|
45
|
+
* > 뜸을 쓰는 훈련 갈래는 파형을 끄고 재생바만 보이는 쪽이 많아 먼저 바를 살렸다.
|
|
30
46
|
*/
|
|
31
47
|
export function timelineProgress(tl, at) {
|
|
32
48
|
const t = trackSegments(tl, at.track);
|
|
33
|
-
if (!t || !(t.totalMs > 0)
|
|
49
|
+
if (!t || !(t.totalMs > 0))
|
|
34
50
|
return 0;
|
|
35
51
|
const 구간 = t[at.kind];
|
|
36
52
|
const 흐른 = Math.min(Math.max(0, Number.isFinite(at.elapsedMs) ? at.elapsedMs : 0), 구간.durMs);
|
|
@@ -206,6 +206,11 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, sequence
|
|
|
206
206
|
const mainPlaying = !!a && !a.paused && !a.ended;
|
|
207
207
|
// speechSynthesis TTS 경로는 main <audio> 없이 재생되므로 playbackState도 함께 본다
|
|
208
208
|
// 뜸 구간은 메인이 멈춰 있어도 **재생 중**이다 — 안 보면 1000ms 뜸의 800ms 에 소음이 꺼진다
|
|
209
|
+
/* `뜸중()` 은 지금 **겹옷이다**(0.288.0 에서 재 봤다). 뜸이 시작될 때 재생기가
|
|
210
|
+
playbackState 를 'playing' 으로 올리므로 오른쪽이 이미 참이다 — 이 셋 가운데
|
|
211
|
+
`뜸중()` 만 지워도 시험 108 개가 다 통과한다. 둘이 같은 뜻이 아니라 **오늘 같이
|
|
212
|
+
갈 뿐**이라 남겨 둔다. 지우려면 "뜸이면 playbackState 가 'playing' 이다" 를 먼저
|
|
213
|
+
문으로 세워라 — 안 세우고 지우면 다음에 그 하나가 안 따라간다. */
|
|
209
214
|
if (mainPlaying || 뜸중() || playbackStateRef.current === 'playing') {
|
|
210
215
|
misses = 0;
|
|
211
216
|
return;
|
|
@@ -314,6 +319,7 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, sequence
|
|
|
314
319
|
// 나오는 버그가 있었다. 메인이 재생 중이 아니면 무조건 정지시킨다.
|
|
315
320
|
const mainAudio = audioRef.current;
|
|
316
321
|
// speechSynthesis TTS 경로는 메인 <audio> 없이 재생된다 — 감시 타이머처럼 playbackState 도 본다(안 보면 비율만 바꿔도 소음이 멈췄다)
|
|
322
|
+
// `뜸중()` 은 여기서도 겹옷이다 — 위 감시 타이머 옆 주석 참고.
|
|
317
323
|
const mainPlaying = (!!mainAudio && !mainAudio.paused && !mainAudio.ended) || 뜸중() || playbackStateRef.current === 'playing';
|
|
318
324
|
if (mainPlaying && pendingNoiseUrlRef.current) {
|
|
319
325
|
/* 종류를 바꾼 뒤 150ms 페이드아웃 안에 다른 설정을 바꾸면, 아래 볼륨만 옮기는 갈래가 파일을 바꿀 페이드를 지워 화면의 종류와
|
|
@@ -97,7 +97,11 @@ export function useProgressTicker({ audioRef, fillRef, waveProgressRef, sourcesR
|
|
|
97
97
|
// 트랙 길이를 알면 progressOf 가 그것을 쓴다 — <audio> 길이는 모를 때만 본다
|
|
98
98
|
const 총길이 = 트랙길이알아 ? 0 : audioDurationOf(a);
|
|
99
99
|
const fill = fillRef.current;
|
|
100
|
-
|
|
100
|
+
/* 뜸이 있으면 **뜸을 포함한 시간선**으로 잰다. 없으면(기본 0) 전과 같은 길이다. */
|
|
101
|
+
const 뜸있다 = (leadInMs || 0) > 0 || (tailMs || 0) > 0;
|
|
102
|
+
/* **지금 뜸이 도는가** — 길이를 몰라도 뜸은 제 길이를 안다(아래 참고) */
|
|
103
|
+
const 뜸중 = 뜸있다 && (sequencer?.positionRef.current?.phase ?? 'sound') !== 'sound';
|
|
104
|
+
if (총길이 === null && !뜸중) {
|
|
101
105
|
/* 길이를 모른다(스트리밍 등으로 duration 이 Infinity·NaN 이고 되감을 수 있는 끝도 없다) —
|
|
102
106
|
마커를 0 에 붙여 두면 "안 움직이는 재생"으로 읽힌다. 진행을 모르니 마커를 숨긴다. */
|
|
103
107
|
try {
|
|
@@ -107,8 +111,15 @@ export function useProgressTicker({ audioRef, fillRef, waveProgressRef, sourcesR
|
|
|
107
111
|
catch { /* 떼어진 노드 */ }
|
|
108
112
|
}
|
|
109
113
|
else {
|
|
110
|
-
/*
|
|
111
|
-
|
|
114
|
+
/* **뜸이 도는 동안에는 길이를 몰라도 숨기지 않는다**(말귀 되돌아감 보고, 2026-10-06).
|
|
115
|
+
앱에서 앞뜸 1초 내내 재생바가 `0.0%` 에 멈췄다가 소리가 나는 순간 `38.3%` 로
|
|
116
|
+
튀었다 — 소리 길이 재기가 0 을 줘서 위 숨김 가지로 들어갔고, 그 1초 동안
|
|
117
|
+
**아무것도 안 그렸다.** 뜸은 제 길이를 아는 구간이다(`leadInMs`·`tailMs` 를
|
|
118
|
+
여기서 받고 있다). 그래서 길이를 모르면 **소리를 0 으로 둔 시간표**로 잰다 —
|
|
119
|
+
`timelineProgress` 에 그 사정을 적어 뒀다. 사용자가 (86) 에서 직접 짚은 자리다. */
|
|
120
|
+
if (isDev && 총길이 === null && 뜸중) {
|
|
121
|
+
devWarnOnce('progress-ticker-pad-without-duration', '[ihab-core] 뜸이 도는데 **소리 길이를 모릅니다** — 재생바가 뜸만으로 어림잡아 움직입니다.', '소리 길이 재기(measureAudioDuration)가 0 을 줬거나 `<audio>` 가 아직 안 실렸습니다. 길이를 알게 되면 마커가 그 순간 **뒤로 물러납니다**(어림은 늘 참보다 큽니다).');
|
|
122
|
+
}
|
|
112
123
|
const pos = 뜸있다
|
|
113
124
|
? progressWithPad({
|
|
114
125
|
phase: sequencer?.positionRef.current?.phase ?? 'sound',
|
|
@@ -122,7 +133,9 @@ export function useProgressTicker({ audioRef, fillRef, waveProgressRef, sourcesR
|
|
|
122
133
|
currentTime: a && typeof a.currentTime === 'number' ? a.currentTime : 0,
|
|
123
134
|
sources: sourcesRef.current,
|
|
124
135
|
index: currentIndexRef.current,
|
|
125
|
-
|
|
136
|
+
/* 여기 올 때 `총길이` 는 널이 아니다 — 널이면 위에서 숨기고 왔다(뜸이 없으면 뜸중도 거짓이다).
|
|
137
|
+
타입만 좁혀 준다. */
|
|
138
|
+
audioDuration: 총길이 ?? 0,
|
|
126
139
|
});
|
|
127
140
|
try {
|
|
128
141
|
if (fill) {
|