@ihabdevteam/core 0.266.0 → 0.268.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 +99 -0
- package/dist/components.cjs.js +24 -24
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +18 -18
- package/dist/ihabdevteam-core.d.ts +15 -2
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +15 -1
- package/dist/packages/core/src/components/Training/TrainingStepListen.d.ts +14 -1
- package/dist/packages/core/src/components/Training/TrainingStepListen.js +2 -2
- package/dist/packages/core/src/components/Training/useNoiseFade.d.ts +13 -1
- package/dist/packages/core/src/components/Training/useNoiseFade.js +16 -9
- 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.268.0";
|
|
1655
1655
|
|
|
1656
1656
|
/** @public */
|
|
1657
1657
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -9039,7 +9039,7 @@ export declare type TrainingStepConfirmWithTextProps = {
|
|
|
9039
9039
|
};
|
|
9040
9040
|
|
|
9041
9041
|
/** @public */
|
|
9042
|
-
export declare function TrainingStepListen({ index, stepIndex, isLast, title, onDone, btnLabel, soundSrc, bucketPath, usePreviewTextBtn, previewText, onPreviewTextBtn, disabled, _pageIndex, showSoundWaveForm, showNoiseWaveForm }: TrainingStepListenProps): JSX.Element;
|
|
9042
|
+
export declare function TrainingStepListen({ index, stepIndex, isLast, title, onDone, btnLabel, soundSrc, bucketPath, usePreviewTextBtn, previewText, onPreviewTextBtn, disabled, _pageIndex, showSoundWaveForm, showNoiseWaveForm, leadInMs, tailMs }: TrainingStepListenProps): JSX.Element;
|
|
9043
9043
|
|
|
9044
9044
|
/** @public */
|
|
9045
9045
|
export declare type TrainingStepListenProps = {
|
|
@@ -9063,6 +9063,19 @@ export declare type TrainingStepListenProps = {
|
|
|
9063
9063
|
showSoundWaveForm?: boolean;
|
|
9064
9064
|
/** 소음 waveform 표시 여부. 기본 true. */
|
|
9065
9065
|
showNoiseWaveForm?: boolean;
|
|
9066
|
+
/**
|
|
9067
|
+
* **소리 앞에 두는 뜸**(밀리초). 기본 0 — 안 주면 전과 똑같이 돈다.
|
|
9068
|
+
* 그대로 `TrainingStepAudioPlayer` 로 간다.
|
|
9069
|
+
*
|
|
9070
|
+
* 앱은 재생기를 직접 세우지 않는다 — 훈련 러너 여섯이 전부
|
|
9071
|
+
* `TrainingPagePopup.ListenStep` 으로 짓고 그 길이 여기를 지난다. 그래서 재생기에만
|
|
9072
|
+
* 더하면 **앱까지 닿지 않는다**(말귀 실측, 2026-10-05: (74) 가 0.266.0 으로 나갔는데
|
|
9073
|
+
* 앱은 임시 자를 못 걷었다). 재생기에 프롭을 더하면 여기도 함께 더한다 —
|
|
9074
|
+
* `test/trainingStepListenForwards.test.ts` 가 빠진 것을 센다.
|
|
9075
|
+
*/
|
|
9076
|
+
leadInMs?: number;
|
|
9077
|
+
/** **소리가 끝난 뒤 뜸**(밀리초). 기본 0. 위 `leadInMs` 와 같은 자리에서 왔다. */
|
|
9078
|
+
tailMs?: number;
|
|
9066
9079
|
};
|
|
9067
9080
|
|
|
9068
9081
|
export declare function TrainingStepOrderList({ index, stepIndex, isLast, title, onDone, onChange, context, }: TrainingStepOrderListProps): JSX.Element;
|
|
@@ -190,6 +190,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
190
190
|
noisePlayer,
|
|
191
191
|
audioRef,
|
|
192
192
|
playbackStateRef,
|
|
193
|
+
padRef: 뜸Ref,
|
|
193
194
|
});
|
|
194
195
|
const { startTTS: 합성시작, stopTTS } = useTTSPlayback({
|
|
195
196
|
ttsText,
|
|
@@ -286,6 +287,19 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
286
287
|
먼저 켠다(소리가 날 때 오는 `play` 이벤트가 아니라 **여기**가 시작이다). */
|
|
287
288
|
if (leadInMsRef.current > 0) {
|
|
288
289
|
startTicker();
|
|
290
|
+
/* **소음은 뜸부터 깔린다.** 사용자가 (74) 를 말할 때 기준으로 삼은 것이 "노이즈가
|
|
291
|
+
나오는 순간" 이었다 — 그 순간이 단어와 같이 오면 뜸을 둔 뜻이 없다. 소리가 터지지
|
|
292
|
+
않게 하려고 둔 구간인데 거기가 **완전한 무음**이면, 단어가 소음과 함께 들이닥치는
|
|
293
|
+
것은 그대로다. 뜸 동안 소음만 깔려 있어야 귀가 먼저 자리를 잡는다.
|
|
294
|
+
`onPlay` 에도 같은 호출이 있다 — 이미 돌고 있으면 `fadeInNoise` 는 지금 볼륨에서
|
|
295
|
+
목표까지 이어 가므로(멀티소스 전환 때문에 그렇게 돼 있다) 두 번 불려도 끊기지 않는다. */
|
|
296
|
+
const ns = noiseSettingsRef.current;
|
|
297
|
+
if (ns?.enabled && (ns.volume ?? 0) > 0) {
|
|
298
|
+
try {
|
|
299
|
+
fadeInNoise(ns.volume);
|
|
300
|
+
}
|
|
301
|
+
catch (e) { }
|
|
302
|
+
}
|
|
289
303
|
if (!(await 뜸기다리기(leadInMsRef.current, 'lead')))
|
|
290
304
|
return false;
|
|
291
305
|
if (!mountedRef.current || 의도 !== 재생의도Ref.current)
|
|
@@ -327,7 +341,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
327
341
|
}
|
|
328
342
|
catch (_) { }
|
|
329
343
|
return false;
|
|
330
|
-
}, [t, startTicker,
|
|
344
|
+
}, [t, startTicker, 뜸기다리기, fadeInNoise, noiseSettingsRef]);
|
|
331
345
|
const attachAudioListeners = useCallback(() => {
|
|
332
346
|
const a = audioRef.current;
|
|
333
347
|
if (!a)
|
|
@@ -22,6 +22,19 @@ export type TrainingStepListenProps = {
|
|
|
22
22
|
showSoundWaveForm?: boolean;
|
|
23
23
|
/** 소음 waveform 표시 여부. 기본 true. */
|
|
24
24
|
showNoiseWaveForm?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* **소리 앞에 두는 뜸**(밀리초). 기본 0 — 안 주면 전과 똑같이 돈다.
|
|
27
|
+
* 그대로 `TrainingStepAudioPlayer` 로 간다.
|
|
28
|
+
*
|
|
29
|
+
* 앱은 재생기를 직접 세우지 않는다 — 훈련 러너 여섯이 전부
|
|
30
|
+
* `TrainingPagePopup.ListenStep` 으로 짓고 그 길이 여기를 지난다. 그래서 재생기에만
|
|
31
|
+
* 더하면 **앱까지 닿지 않는다**(말귀 실측, 2026-10-05: (74) 가 0.266.0 으로 나갔는데
|
|
32
|
+
* 앱은 임시 자를 못 걷었다). 재생기에 프롭을 더하면 여기도 함께 더한다 —
|
|
33
|
+
* `test/trainingStepListenForwards.test.ts` 가 빠진 것을 센다.
|
|
34
|
+
*/
|
|
35
|
+
leadInMs?: number;
|
|
36
|
+
/** **소리가 끝난 뒤 뜸**(밀리초). 기본 0. 위 `leadInMs` 와 같은 자리에서 왔다. */
|
|
37
|
+
tailMs?: number;
|
|
25
38
|
};
|
|
26
39
|
/** @public */
|
|
27
|
-
export default function TrainingStepListen({ index, stepIndex, isLast, title, onDone, btnLabel, soundSrc, bucketPath, usePreviewTextBtn, previewText, onPreviewTextBtn, disabled, _pageIndex, showSoundWaveForm, showNoiseWaveForm }: TrainingStepListenProps): import("react").JSX.Element;
|
|
40
|
+
export default function TrainingStepListen({ index, stepIndex, isLast, title, onDone, btnLabel, soundSrc, bucketPath, usePreviewTextBtn, previewText, onPreviewTextBtn, disabled, _pageIndex, showSoundWaveForm, showNoiseWaveForm, leadInMs, tailMs }: TrainingStepListenProps): import("react").JSX.Element;
|
|
@@ -5,9 +5,9 @@ import TrainingStepAudioPlayer from './TrainingStepAudioPlayer';
|
|
|
5
5
|
import "./TrainingStep.css";
|
|
6
6
|
import TrainingStep from "./TrainingStep";
|
|
7
7
|
/** @public */
|
|
8
|
-
export default function TrainingStepListen({ index, stepIndex, isLast = false, title, onDone, btnLabel, soundSrc, bucketPath, usePreviewTextBtn, previewText = null, onPreviewTextBtn, disabled, _pageIndex, showSoundWaveForm, showNoiseWaveForm }) {
|
|
8
|
+
export default function TrainingStepListen({ index, stepIndex, isLast = false, title, onDone, btnLabel, soundSrc, bucketPath, usePreviewTextBtn, previewText = null, onPreviewTextBtn, disabled, _pageIndex, showSoundWaveForm, showNoiseWaveForm, leadInMs, tailMs }) {
|
|
9
9
|
const { t } = useTranslation();
|
|
10
10
|
const resolvedTitle = title ?? t('training.listenTitle', '제시하는 문장을 들어주세요.');
|
|
11
11
|
const effectiveDisabled = disabled ?? (typeof stepIndex === 'number' && typeof index === 'number' ? index > stepIndex : false);
|
|
12
|
-
return (_jsx(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, title: resolvedTitle, onDone: onDone, titleProps: { usePreviewTextBtn, previewText, onPreviewTextBtn }, children: _jsx(TrainingStepAudioPlayer, { soundSrc: soundSrc, ttsText: !soundSrc ? (previewText ?? resolvedTitle) : null, bucketPath: bucketPath, btnLabel: btnLabel || t('training.listenButton', '문장 듣기'), disabled: effectiveDisabled, className: "training-step__listen-sticky", showSoundWaveForm: showSoundWaveForm, showNoiseWaveForm: showNoiseWaveForm }, `audio-p${_pageIndex ?? 0}-s${index}`) }));
|
|
12
|
+
return (_jsx(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, title: resolvedTitle, onDone: onDone, titleProps: { usePreviewTextBtn, previewText, onPreviewTextBtn }, children: _jsx(TrainingStepAudioPlayer, { soundSrc: soundSrc, ttsText: !soundSrc ? (previewText ?? resolvedTitle) : null, bucketPath: bucketPath, btnLabel: btnLabel || t('training.listenButton', '문장 듣기'), disabled: effectiveDisabled, className: "training-step__listen-sticky", showSoundWaveForm: showSoundWaveForm, showNoiseWaveForm: showNoiseWaveForm, leadInMs: leadInMs, tailMs: tailMs }, `audio-p${_pageIndex ?? 0}-s${index}`) }));
|
|
13
13
|
}
|
|
@@ -5,10 +5,22 @@ type PlaybackState = 'stopped' | 'playing' | 'paused';
|
|
|
5
5
|
* TrainingStepAudioPlayer에서 분리 — 소음(noise) 오디오 엘리먼트 생성/페이드인/페이드아웃/워치독을
|
|
6
6
|
* 전담한다. audioRef(메인 문장 오디오)·playbackStateRef는 메인 컴포넌트 소유라 그대로 전달받는다.
|
|
7
7
|
*/
|
|
8
|
-
export declare function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, }: {
|
|
8
|
+
export declare function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, padRef, }: {
|
|
9
9
|
noisePlayer: NoisePlayerContextValue | null;
|
|
10
10
|
audioRef: RefObject<HTMLAudioElement | null>;
|
|
11
11
|
playbackStateRef: RefObject<PlaybackState>;
|
|
12
|
+
/**
|
|
13
|
+
* **앞뒤 뜸 구간인가**(말귀 요청 (74), 2026-10-05).
|
|
14
|
+
*
|
|
15
|
+
* 뜸 동안 메인 `<audio>` 는 **멈춰 있다** — 앞 뜸에는 아직 `play()` 전이고, 뒤 뜸에는
|
|
16
|
+
* 이미 `ended` 다. 그래서 "메인이 도나" 만 보는 자리들이 **뜸을 재생이 끝난 것으로 읽어**
|
|
17
|
+
* 소음을 걷는다. 특히 아래 감시 타이머는 0.8초 뒤에 내리므로, 1000ms 뜸은 **800ms 에서
|
|
18
|
+
* 소음이 꺼진다.** 뜸은 소리가 안 나는 구간이지 **재생이 끝난 구간이 아니다.**
|
|
19
|
+
*/
|
|
20
|
+
padRef?: RefObject<{
|
|
21
|
+
phase: 'lead' | 'sound' | 'tail';
|
|
22
|
+
at: number;
|
|
23
|
+
}>;
|
|
12
24
|
}): {
|
|
13
25
|
noiseAudioRef: RefObject<HTMLAudioElement | null>;
|
|
14
26
|
activeNoiseUrl: string | null;
|
|
@@ -17,7 +17,7 @@ function generateNoiseUrl(type) {
|
|
|
17
17
|
* TrainingStepAudioPlayer에서 분리 — 소음(noise) 오디오 엘리먼트 생성/페이드인/페이드아웃/워치독을
|
|
18
18
|
* 전담한다. audioRef(메인 문장 오디오)·playbackStateRef는 메인 컴포넌트 소유라 그대로 전달받는다.
|
|
19
19
|
*/
|
|
20
|
-
export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, }) {
|
|
20
|
+
export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, padRef, }) {
|
|
21
21
|
const registerAnalyserRef = useRef(noisePlayer?.registerAnalyser);
|
|
22
22
|
registerAnalyserRef.current = noisePlayer?.registerAnalyser;
|
|
23
23
|
const noiseAudioRef = useRef(null);
|
|
@@ -35,6 +35,11 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, }) {
|
|
|
35
35
|
// 판별해, 이미 다른 의도(fadeIn 등)로 대체된 정지를 실행하지 않게 한다.
|
|
36
36
|
const noiseFadeTokenRef = useRef(0);
|
|
37
37
|
const noiseHardStopTimerRef = useRef(null);
|
|
38
|
+
/** 지금 뜸 구간인가 — `at` 이 0 이면 뜸이 아니다(소리 구간으로 돌려놓은 표시) */
|
|
39
|
+
const 뜸중 = useCallback(() => {
|
|
40
|
+
const p = padRef?.current;
|
|
41
|
+
return !!p && p.phase !== 'sound' && p.at > 0;
|
|
42
|
+
}, [padRef]);
|
|
38
43
|
const cancelNoiseFade = useCallback(() => {
|
|
39
44
|
if (noiseFadeRafRef.current !== null) {
|
|
40
45
|
cancelAnimationFrame(noiseFadeRafRef.current);
|
|
@@ -201,7 +206,8 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, }) {
|
|
|
201
206
|
const a = audioRef.current;
|
|
202
207
|
const mainPlaying = !!a && !a.paused && !a.ended;
|
|
203
208
|
// speechSynthesis TTS 경로는 main <audio> 없이 재생되므로 playbackState도 함께 본다
|
|
204
|
-
|
|
209
|
+
// 뜸 구간은 메인이 멈춰 있어도 **재생 중**이다 — 안 보면 1000ms 뜸의 800ms 에 소음이 꺼진다
|
|
210
|
+
if (mainPlaying || 뜸중() || playbackStateRef.current === 'playing') {
|
|
205
211
|
misses = 0;
|
|
206
212
|
return;
|
|
207
213
|
}
|
|
@@ -212,7 +218,7 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, }) {
|
|
|
212
218
|
}
|
|
213
219
|
}, 400);
|
|
214
220
|
return () => clearInterval(iv);
|
|
215
|
-
}, [fadeOutNoise, noiseInUse, audioRef, playbackStateRef]);
|
|
221
|
+
}, [fadeOutNoise, noiseInUse, audioRef, playbackStateRef, 뜸중]);
|
|
216
222
|
// 탭 백그라운드 전환 시 소음 즉시 정지 — 백그라운드 탭에서는 requestAnimationFrame이
|
|
217
223
|
// 멈추거나 크게 스로틀돼 fadeOutNoise의 볼륨 램프가 완주하지 못할 수 있어, 페이드 없이
|
|
218
224
|
// 바로 pause한다.
|
|
@@ -235,14 +241,14 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, }) {
|
|
|
235
241
|
if (!noiseEl || !noiseEl.paused)
|
|
236
242
|
return;
|
|
237
243
|
const a = audioRef.current;
|
|
238
|
-
const mainPlaying = (!!a && !a.paused && !a.ended) || playbackStateRef.current === 'playing';
|
|
244
|
+
const mainPlaying = (!!a && !a.paused && !a.ended) || 뜸중() || playbackStateRef.current === 'playing';
|
|
239
245
|
const 지금 = noiseSettingsRef.current;
|
|
240
246
|
if (mainPlaying && 지금?.enabled && 지금.volume > 0)
|
|
241
247
|
fadeInNoise(지금.volume);
|
|
242
248
|
};
|
|
243
249
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
244
250
|
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
245
|
-
}, [cancelNoiseFade, fadeInNoise, audioRef, playbackStateRef]);
|
|
251
|
+
}, [cancelNoiseFade, fadeInNoise, audioRef, playbackStateRef, 뜸중]);
|
|
246
252
|
// noiseSettingsRef 최신화 (재생 이벤트 핸들러 클로저에서 사용)
|
|
247
253
|
useEffect(() => {
|
|
248
254
|
noiseSettingsRef.current = noisePlayer
|
|
@@ -278,7 +284,7 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, }) {
|
|
|
278
284
|
noiseEl.src = url;
|
|
279
285
|
noiseEl.load();
|
|
280
286
|
const mainAudio = audioRef.current;
|
|
281
|
-
if (mainAudio && !mainAudio.paused) {
|
|
287
|
+
if ((mainAudio && !mainAudio.paused) || 뜸중()) {
|
|
282
288
|
canplay떼기Ref.current?.();
|
|
283
289
|
const h = () => {
|
|
284
290
|
noiseEl.removeEventListener('canplay', h);
|
|
@@ -290,7 +296,8 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, }) {
|
|
|
290
296
|
껐어도 canplay 에서 1 로 다시 켜져 끝까지 났다(난이도가 뜻보다 어려워지고 기록과 들린 소리가 어긋남). */
|
|
291
297
|
const cur = audioRef.current;
|
|
292
298
|
const 지금 = noiseSettingsRef.current;
|
|
293
|
-
|
|
299
|
+
const 도나 = (!!cur && !cur.paused && !cur.ended) || 뜸중();
|
|
300
|
+
if (도나 && 지금?.enabled && 지금.volume > 0)
|
|
294
301
|
fadeInNoise(지금.volume);
|
|
295
302
|
};
|
|
296
303
|
const 떼기 = () => noiseEl.removeEventListener('canplay', h);
|
|
@@ -308,7 +315,7 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, }) {
|
|
|
308
315
|
// 나오는 버그가 있었다. 메인이 재생 중이 아니면 무조건 정지시킨다.
|
|
309
316
|
const mainAudio = audioRef.current;
|
|
310
317
|
// speechSynthesis TTS 경로는 메인 <audio> 없이 재생된다 — 감시 타이머처럼 playbackState 도 본다(안 보면 비율만 바꿔도 소음이 멈췄다)
|
|
311
|
-
const mainPlaying = (!!mainAudio && !mainAudio.paused && !mainAudio.ended) || playbackStateRef.current === 'playing';
|
|
318
|
+
const mainPlaying = (!!mainAudio && !mainAudio.paused && !mainAudio.ended) || 뜸중() || playbackStateRef.current === 'playing';
|
|
312
319
|
if (mainPlaying && pendingNoiseUrlRef.current) {
|
|
313
320
|
/* 종류를 바꾼 뒤 150ms 페이드아웃 안에 다른 설정을 바꾸면, 아래 볼륨만 옮기는 갈래가 파일을 바꿀 페이드를 지워 화면의 종류와
|
|
314
321
|
들리는 종류가 문장 끝까지 달랐다 — 기다리는 파일이 있으면 fadeInNoise 가 그것을 끼운다. */
|
|
@@ -342,7 +349,7 @@ export function useNoiseFade({ noisePlayer, audioRef, playbackStateRef, }) {
|
|
|
342
349
|
}
|
|
343
350
|
}
|
|
344
351
|
}
|
|
345
|
-
}, [noisePlayer?.settings, noisePlayer?.settings.enabled, noiseVolume, noisePlayer?.settings.type, cancelNoiseFade, fadeInNoise, fadeOutNoise, audioRef, playbackStateRef, noiseSettingsRef]);
|
|
352
|
+
}, [noisePlayer?.settings, noisePlayer?.settings.enabled, noiseVolume, noisePlayer?.settings.type, cancelNoiseFade, fadeInNoise, fadeOutNoise, audioRef, playbackStateRef, noiseSettingsRef, 뜸중]);
|
|
346
353
|
// 소음 재생 속도(playbackRate) 반영 — 0.75~1.25. 엘리먼트 속성이라 src 변경에도 유지됨
|
|
347
354
|
useEffect(() => {
|
|
348
355
|
const el = noiseAudioRef.current;
|