@ihabdevteam/core 0.321.0 → 0.323.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +617 -0
- package/dist/components.cjs.js +183 -183
- package/dist/components.esm.css +1 -1
- package/dist/hooks.cjs.js +3 -3
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core.cjs.js +187 -187
- package/dist/ihabdevteam-core.d.ts +108 -66
- package/dist/packages/core/src/components/Accordion.js +3 -3
- package/dist/packages/core/src/components/Alert.js +2 -2
- package/dist/packages/core/src/components/BarChart.js +120 -119
- package/dist/packages/core/src/components/BottomNav.js +18 -18
- package/dist/packages/core/src/components/BulkBar.js +25 -25
- package/dist/packages/core/src/components/Button.js +16 -16
- package/dist/packages/core/src/components/Calendar.js +17 -17
- package/dist/packages/core/src/components/ChartSection.js +30 -30
- package/dist/packages/core/src/components/CountTile.js +38 -38
- package/dist/packages/core/src/components/DatePicker.js +59 -59
- package/dist/packages/core/src/components/Dialog.js +14 -14
- package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.js +8 -8
- package/dist/packages/core/src/components/FilterPanel.js +6 -6
- package/dist/packages/core/src/components/FilterSectionInfo.js +6 -6
- package/dist/packages/core/src/components/GameIntro.js +1 -1
- package/dist/packages/core/src/components/HearingDeviceInfo.d.ts +5 -5
- package/dist/packages/core/src/components/HearingDeviceInfo.js +2 -2
- package/dist/packages/core/src/components/Icon.js +2 -2
- package/dist/packages/core/src/components/ItemActions.js +16 -16
- package/dist/packages/core/src/components/LandingHeader.js +6 -6
- package/dist/packages/core/src/components/LineChart.js +24 -24
- package/dist/packages/core/src/components/Loader.js +5 -5
- package/dist/packages/core/src/components/NoticeBox.js +3 -3
- package/dist/packages/core/src/components/PageTitle.js +2 -2
- package/dist/packages/core/src/components/Pagination.js +4 -4
- package/dist/packages/core/src/components/Panel.js +2 -2
- package/dist/packages/core/src/components/PanelRow.d.ts +2 -4
- package/dist/packages/core/src/components/PanelRow.js +8 -15
- package/dist/packages/core/src/components/PassCard/PassCard.js +9 -9
- package/dist/packages/core/src/components/PassCard/passCardProduct.js +2 -2
- package/dist/packages/core/src/components/PaymentPlan.js +5 -5
- package/dist/packages/core/src/components/Popup/ConfirmPopup.js +29 -29
- package/dist/packages/core/src/components/Popup/CouponInputPopup.js +2 -2
- package/dist/packages/core/src/components/Popup/DeviceListPopup.d.ts +4 -8
- package/dist/packages/core/src/components/Popup/DeviceListPopup.js +1 -1
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +19 -26
- package/dist/packages/core/src/components/Popup/ListPopup.js +6 -6
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +10 -10
- package/dist/packages/core/src/components/Popup/Popup.js +27 -27
- package/dist/packages/core/src/components/Popup/SaveTablePopup.js +5 -5
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +4 -4
- package/dist/packages/core/src/components/Popup/customerDetailUtils.js +18 -18
- package/dist/packages/core/src/components/Popup/useDiscardGuard.js +4 -4
- package/dist/packages/core/src/components/ProgressIndicator.js +5 -5
- package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +16 -16
- package/dist/packages/core/src/components/Rating.js +20 -20
- package/dist/packages/core/src/components/Report/ReportDocument.js +21 -21
- package/dist/packages/core/src/components/RingChart.js +5 -5
- package/dist/packages/core/src/components/SectionHeadFloat.js +15 -15
- package/dist/packages/core/src/components/SegmentedControl.js +34 -34
- package/dist/packages/core/src/components/SideNav.js +37 -37
- package/dist/packages/core/src/components/Slider.js +2 -2
- package/dist/packages/core/src/components/SoundCompass.js +5 -5
- package/dist/packages/core/src/components/Table.js +257 -98
- package/dist/packages/core/src/components/Tabs.js +12 -12
- package/dist/packages/core/src/components/TextField.js +34 -34
- package/dist/packages/core/src/components/Toast.js +6 -6
- package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +4 -4
- package/dist/packages/core/src/components/Tooltip/Tooltip.js +22 -22
- package/dist/packages/core/src/components/Tooltip/TrainingTooltip.js +4 -4
- package/dist/packages/core/src/components/TopBanner.js +28 -28
- package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +11 -11
- package/dist/packages/core/src/components/Training/CategoryIcon.js +2 -2
- package/dist/packages/core/src/components/Training/NoisePlayerContext.d.ts +7 -4
- package/dist/packages/core/src/components/Training/NoisePlayerContext.js +12 -12
- package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingBlock.js +12 -12
- package/dist/packages/core/src/components/Training/TrainingBlockButton.js +10 -10
- package/dist/packages/core/src/components/Training/TrainingBlockGrid.js +21 -21
- package/dist/packages/core/src/components/Training/TrainingCard.css +13 -1
- package/dist/packages/core/src/components/Training/TrainingCard.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingCardGroup.js +6 -6
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +83 -83
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +17 -17
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +128 -128
- package/dist/packages/core/src/components/Training/TrainingStepCard.js +18 -18
- package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +4 -4
- package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +12 -12
- package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +13 -13
- package/dist/packages/core/src/components/Training/TrainingStepTitle.js +4 -4
- package/dist/packages/core/src/components/Training/TrainingWordCard.js +17 -17
- package/dist/packages/core/src/components/Training/WordTablePopup.js +5 -5
- package/dist/packages/core/src/components/Training/audioPlaybackRules.js +8 -8
- package/dist/packages/core/src/components/Training/audioTimeline.js +25 -25
- package/dist/packages/core/src/components/Training/oneShotSound.d.ts +1 -1
- package/dist/packages/core/src/components/Training/oneShotSound.js +14 -14
- package/dist/packages/core/src/components/Training/stepAnswerable.d.ts +2 -2
- package/dist/packages/core/src/components/Training/trainingMissStats.js +22 -22
- package/dist/packages/core/src/components/Training/trainingPopupUtils.js +7 -7
- package/dist/packages/core/src/components/Training/useAudioPreview.js +15 -15
- package/dist/packages/core/src/components/Training/useFloatingFocus.js +14 -14
- package/dist/packages/core/src/components/Training/useNoiseFade.js +22 -22
- package/dist/packages/core/src/components/Training/useObjectUrls.js +2 -2
- package/dist/packages/core/src/components/Training/usePlaybackSequencer.d.ts +2 -2
- package/dist/packages/core/src/components/Training/usePlaybackSequencer.js +23 -23
- package/dist/packages/core/src/components/Training/useProgressTicker.js +33 -33
- package/dist/packages/core/src/components/Training/useTTSPlayback.js +17 -17
- package/dist/packages/core/src/components/Training/useWaveReady.js +11 -11
- package/dist/packages/core/src/components/UpdateAlert.js +6 -6
- package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +7 -1
- package/dist/packages/core/src/components/Word/TableFilterPanel.js +3 -3
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +18 -18
- package/dist/packages/core/src/components/Word/WordTableBlock.js +28 -28
- package/dist/packages/core/src/components/Word/WordTableButton.css +16 -2
- package/dist/packages/core/src/components/Word/WordTableThumbnail.css +6 -1
- package/dist/packages/core/src/components/Word/generatedDraft.js +14 -14
- package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -7
- package/dist/packages/core/src/components/Word/wordScoreStats.js +17 -17
- package/dist/packages/core/src/components/Word/wordTablePrint.js +10 -10
- package/dist/packages/core/src/components/Word/wordTableRecommend.js +13 -13
- package/dist/packages/core/src/components/Word/wordTableReview.js +28 -28
- package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +8 -11
- package/dist/packages/core/src/components/Word/wordTableSettings.js +48 -36
- package/dist/packages/core/src/components/WordTable.js +130 -130
- package/dist/packages/core/src/components/barChartData.d.ts +16 -1
- package/dist/packages/core/src/components/barChartData.js +49 -29
- package/dist/packages/core/src/components/barChartTypes.js +8 -8
- package/dist/packages/core/src/components/tableTrackList.js +11 -11
- package/dist/packages/core/src/components/updateAlertStack.js +28 -28
- package/dist/packages/core/src/components/useBarChartControl.js +4 -6
- package/dist/packages/core/src/components/useComboboxSuggestions.js +17 -17
- package/dist/packages/core/src/components/useToastLifecycle.js +18 -18
- package/dist/packages/core/src/components/wordTableLayout.d.ts +22 -7
- package/dist/packages/core/src/components/wordTableLayout.js +5 -5
- package/dist/packages/core/src/context/I18nProvider.js +6 -6
- package/dist/packages/core/src/hooks/focusReturn.js +27 -27
- package/dist/packages/core/src/hooks/inputModality.d.ts +1 -1
- package/dist/packages/core/src/hooks/inputModality.js +14 -14
- package/dist/packages/core/src/hooks/touchToggle.js +39 -39
- package/dist/packages/core/src/hooks/useAnchoredMenu.js +30 -30
- package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +28 -28
- package/dist/packages/core/src/hooks/useDeviceManagement.d.ts +5 -17
- package/dist/packages/core/src/hooks/useDeviceManagement.js +20 -8
- package/dist/packages/core/src/hooks/useDocumentTitle.js +10 -10
- package/dist/packages/core/src/hooks/useEditableList.js +4 -4
- package/dist/packages/core/src/hooks/useFocusTrap.js +2 -2
- package/dist/packages/core/src/hooks/useFontScale.js +6 -6
- package/dist/packages/core/src/hooks/useIcon.js +7 -7
- package/dist/packages/core/src/hooks/useListControls.js +2 -2
- package/dist/packages/core/src/hooks/usePageResetOnChange.d.ts +3 -3
- package/dist/packages/core/src/hooks/usePageResetOnChange.js +6 -6
- package/dist/packages/core/src/hooks/usePasswordChange.js +4 -4
- package/dist/packages/core/src/hooks/usePendingPress.js +7 -7
- package/dist/packages/core/src/hooks/useRelativeDay.js +4 -4
- package/dist/packages/core/src/hooks/useSoundCompassExam.js +6 -6
- package/dist/packages/core/src/hooks/useTablePaging.d.ts +7 -7
- package/dist/packages/core/src/hooks/useTablePaging.js +40 -34
- package/dist/packages/core/src/hooks/useTrainingSessionLog.js +35 -35
- package/dist/packages/core/src/hooks/useVisitSchedule.js +4 -4
- package/dist/packages/core/src/hooks/useWordAttributeFilter.js +14 -14
- package/dist/packages/core/src/hooks/useWordMeanings.js +5 -5
- package/dist/packages/core/src/hooks/useWordTrainingFilter.d.ts +3 -1
- package/dist/packages/core/src/hooks/useWordTrainingFilter.js +14 -12
- package/dist/packages/core/src/i18n/locales/en.json +13 -2
- package/dist/packages/core/src/i18n/locales/ko.json +12 -1
- package/dist/packages/core/src/types/wordSearch.d.ts +9 -0
- package/dist/packages/core/src/utils/audioMeter.js +5 -5
- package/dist/packages/core/src/utils/audioPeaks.d.ts +1 -1
- package/dist/packages/core/src/utils/audioPeaks.js +18 -18
- package/dist/packages/core/src/utils/blockGrid.js +4 -4
- package/dist/packages/core/src/utils/chunkSkew.js +9 -9
- package/dist/packages/core/src/utils/clipboard.js +3 -3
- package/dist/packages/core/src/utils/dailySelectionUtils.js +12 -12
- package/dist/packages/core/src/utils/date.js +12 -12
- package/dist/packages/core/src/utils/devWarn.d.ts +2 -2
- package/dist/packages/core/src/utils/devWarn.js +7 -7
- package/dist/packages/core/src/utils/deviceKind.d.ts +37 -7
- package/dist/packages/core/src/utils/deviceKind.js +53 -10
- package/dist/packages/core/src/utils/errorMessage.js +7 -7
- package/dist/packages/core/src/utils/filterPanelSummary.d.ts +1 -1
- package/dist/packages/core/src/utils/filterPanelSummary.js +16 -16
- package/dist/packages/core/src/utils/format.js +4 -4
- package/dist/packages/core/src/utils/getTrainingData.js +9 -9
- package/dist/packages/core/src/utils/imageConfig.js +17 -17
- package/dist/packages/core/src/utils/koreanSyllable.js +18 -18
- package/dist/packages/core/src/utils/navScroll.d.ts +2 -2
- package/dist/packages/core/src/utils/navScroll.js +6 -6
- package/dist/packages/core/src/utils/phonemeConvert.d.ts +7 -0
- package/dist/packages/core/src/utils/phonemeConvert.js +19 -10
- package/dist/packages/core/src/utils/phonemeGroups.js +2 -2
- package/dist/packages/core/src/utils/phonemePairSearch.d.ts +1 -0
- package/dist/packages/core/src/utils/phonemePairSearch.js +118 -102
- package/dist/packages/core/src/utils/phonemeSectionCodes.d.ts +52 -0
- package/dist/packages/core/src/utils/phonemeSectionCodes.js +76 -0
- package/dist/packages/core/src/utils/prefetchChunk.js +6 -6
- package/dist/packages/core/src/utils/pressable.d.ts +1 -1
- package/dist/packages/core/src/utils/pressable.js +3 -3
- package/dist/packages/core/src/utils/safeHref.js +4 -4
- package/dist/packages/core/src/utils/savedWordTables.js +5 -5
- package/dist/packages/core/src/utils/sessionTab.d.ts +7 -7
- package/dist/packages/core/src/utils/sessionTab.js +9 -9
- package/dist/packages/core/src/utils/speak.js +12 -12
- package/dist/packages/core/src/utils/textSort.d.ts +5 -5
- package/dist/packages/core/src/utils/textSort.js +47 -47
- package/dist/packages/core/src/utils/theme.js +3 -3
- package/dist/packages/core/src/utils/trainingItemResults.js +3 -3
- package/dist/packages/core/src/utils/ttsConfig.js +2 -2
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +2 -2
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +48 -48
- package/dist/packages/core/src/utils/wordContrast.js +3 -3
- package/dist/packages/core/src/utils/wordPairIndex.js +4 -4
- package/dist/packages/core/src/utils/wordTrainingGenerators.js +4 -4
- package/dist/packages/core/src/utils/wordTrainingPool.js +15 -15
- package/dist/packages/core/src/utils/wordTrainingRuntime.js +6 -6
- package/dist/packages/core/src/utils/wornDate.d.ts +3 -3
- package/dist/packages/core/src/utils/wornDate.js +14 -14
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +4 -4
- package/package.json +1 -1
|
@@ -29,7 +29,7 @@ import { loadNoiseSettings, saveNoiseSettings, reportAppliedNoiseSettings } from
|
|
|
29
29
|
/** 판 중 소음 설정 변경을 저장하기 전 기다리는 시간(ms) — 멈춘 뒤 이만큼 지나면 마지막 값만 저장한다. */
|
|
30
30
|
const NOISE_SAVE_DEBOUNCE_MS = 300;
|
|
31
31
|
/* 움직임 최소화면 닫힘 대기를 0 으로 — 사라지는 애니메이션을 보여 줄 까닭이 없다. (공용 훅이 생기면 그것으로 합친다.) */
|
|
32
|
-
const
|
|
32
|
+
const prefersReducedMotion = () => {
|
|
33
33
|
try {
|
|
34
34
|
return typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
35
35
|
}
|
|
@@ -38,8 +38,8 @@ const 움직임최소화 = () => {
|
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
/** 결과를 계산하는 중인 결과 창 자리 — 이 객체와 **같은 것**이면 계산 중이다(결과 창의 '완료' 를 막는다). */
|
|
41
|
-
const
|
|
42
|
-
import {
|
|
41
|
+
const RESULT_PENDING = Object.freeze({});
|
|
42
|
+
import { wasKeyboardPress } from '../../hooks/inputModality';
|
|
43
43
|
// 문장 4종은 각 PagePopup 래퍼에서 useTrainingSessionLog로 이미 기록 → 베이스에서 중복 방지
|
|
44
44
|
const SENTENCE_TIME_LOGGED_TYPES = new Set(['LISTEN_IN_NOISE', 'SHORT_STORY', 'LONG_STORY', 'ORDERING_SENTENCE']);
|
|
45
45
|
/** 결과 창 자료에서 정답/전체를 센다 — 결과 항목 형태가 훈련 타입마다 달라(소음 하 듣기/문장 순서화는 correct 대신
|
|
@@ -57,7 +57,7 @@ const SENTENCE_TIME_LOGGED_TYPES = new Set(['LISTEN_IN_NOISE', 'SHORT_STORY', 'L
|
|
|
57
57
|
* > 안 터지다가 옆 함수에서는 이미 터져 있었다). 여기는 **뜻이 같아서** 안 터진다 —
|
|
58
58
|
* > 그 둘이 갈라지는 날이 오면 이 자리가 첫 판이다.
|
|
59
59
|
*/
|
|
60
|
-
function
|
|
60
|
+
function countRoundResult(resultOverlay) {
|
|
61
61
|
const ro = resultOverlay;
|
|
62
62
|
/* `results` 가 이긴다 — 둘 다 오면 그쪽이 코어가 지은 이름이다 */
|
|
63
63
|
const results = (ro?.results || ro?.answers || []);
|
|
@@ -211,28 +211,28 @@ function TrainingPagePopupInner(props) {
|
|
|
211
211
|
}, []);
|
|
212
212
|
/* 판 중 변경의 저장은 **멈춘 뒤 300ms 에 마지막 값만** 보낸다 — 전에는 슬라이더를 끄는 동안 움직일 때마다 저장이 나갔다
|
|
213
213
|
(한 번 끌면 수십 번의 네트워크 쓰기, 늦게 도착한 앞 값이 뒤 값을 덮을 수도). 떠날 때·pagehide·탭 숨김에는 남은 값을 즉시 보낸다. */
|
|
214
|
-
const
|
|
215
|
-
const
|
|
214
|
+
const pendingSaveRef = useRef(null);
|
|
215
|
+
const saveTimerRef = useRef(null);
|
|
216
216
|
const flushNoiseSave = useCallback(() => {
|
|
217
|
-
if (
|
|
218
|
-
clearTimeout(
|
|
219
|
-
|
|
217
|
+
if (saveTimerRef.current !== null) {
|
|
218
|
+
clearTimeout(saveTimerRef.current);
|
|
219
|
+
saveTimerRef.current = null;
|
|
220
220
|
}
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
saveNoiseSettings(
|
|
221
|
+
const pending = pendingSaveRef.current;
|
|
222
|
+
pendingSaveRef.current = null;
|
|
223
|
+
if (pending)
|
|
224
|
+
saveNoiseSettings(pending);
|
|
225
225
|
}, []);
|
|
226
226
|
useEffect(() => {
|
|
227
227
|
if (typeof window === 'undefined')
|
|
228
228
|
return;
|
|
229
|
-
const
|
|
229
|
+
const onHidden = () => { if (document.visibilityState === 'hidden')
|
|
230
230
|
flushNoiseSave(); };
|
|
231
231
|
window.addEventListener('pagehide', flushNoiseSave);
|
|
232
|
-
document.addEventListener('visibilitychange',
|
|
232
|
+
document.addEventListener('visibilitychange', onHidden);
|
|
233
233
|
return () => {
|
|
234
234
|
window.removeEventListener('pagehide', flushNoiseSave);
|
|
235
|
-
document.removeEventListener('visibilitychange',
|
|
235
|
+
document.removeEventListener('visibilitychange', onHidden);
|
|
236
236
|
flushNoiseSave();
|
|
237
237
|
};
|
|
238
238
|
}, [flushNoiseSave]);
|
|
@@ -249,10 +249,10 @@ function TrainingPagePopupInner(props) {
|
|
|
249
249
|
}
|
|
250
250
|
// noiseOverride 판은 저장하지 않는다 — 판 중에 바꾼 것도 이 판에만.
|
|
251
251
|
if (!noiseOverrideRef.current) {
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
clearTimeout(
|
|
255
|
-
|
|
252
|
+
pendingSaveRef.current = merged;
|
|
253
|
+
if (saveTimerRef.current !== null)
|
|
254
|
+
clearTimeout(saveTimerRef.current);
|
|
255
|
+
saveTimerRef.current = setTimeout(flushNoiseSave, NOISE_SAVE_DEBOUNCE_MS);
|
|
256
256
|
}
|
|
257
257
|
reportAppliedNoiseSettings(merged);
|
|
258
258
|
}, [flushNoiseSave]);
|
|
@@ -278,7 +278,7 @@ function TrainingPagePopupInner(props) {
|
|
|
278
278
|
비워, 같은 문항을 다시 풀면 옛 선택 강조·옛 배치(순서화는 정답 배치일 수도)가 그대로 남았다. */
|
|
279
279
|
const [replayCount, setReplayCount] = useState(0);
|
|
280
280
|
// 다시하기 전 판들의 정답/전체 — 완료 때 onComplete 에 함께 넘긴다
|
|
281
|
-
const
|
|
281
|
+
const previousRoundsRef = useRef([]);
|
|
282
282
|
const [mode, setMode] = useState('center');
|
|
283
283
|
const [closing, setClosing] = useState(false);
|
|
284
284
|
// 닫힘 애니메이션(PAGE_POPUP_CLOSE_MS) 동안 다른 닫기 경로(ESC/닫기 버튼/완료)가 또 실행되면
|
|
@@ -300,7 +300,7 @@ function TrainingPagePopupInner(props) {
|
|
|
300
300
|
closingRef.current = false;
|
|
301
301
|
setClosing(false);
|
|
302
302
|
}
|
|
303
|
-
},
|
|
303
|
+
}, prefersReducedMotion() ? 0 : PAGE_POPUP_CLOSE_MS);
|
|
304
304
|
}, []);
|
|
305
305
|
useEffect(() => () => { if (closeTimerRef.current)
|
|
306
306
|
clearTimeout(closeTimerRef.current); }, []);
|
|
@@ -350,7 +350,7 @@ function TrainingPagePopupInner(props) {
|
|
|
350
350
|
const [resultOverlay, setResultOverlay] = useState(null);
|
|
351
351
|
/* 결과 요청 세대 — 결과보기가 올리고, 창을 닫거나(다시하기·완료·시나리오 초기화) 새로 요청하면 앞 요청은 늦은 것이 된다.
|
|
352
352
|
결과 계산이 비동기일 때 늦게 온 옛 결과가 새 판 위에 결과 창을 다시 띄우거나 2회차 결과를 1회차 것으로 덮었다. */
|
|
353
|
-
const
|
|
353
|
+
const resultGenerationRef = useRef(0);
|
|
354
354
|
/**
|
|
355
355
|
* **판 번호** — 한 판은 결과창이 닫힐 때 끝난다(다시하기도 닫고 시작한다).
|
|
356
356
|
*
|
|
@@ -359,8 +359,8 @@ function TrainingPagePopupInner(props) {
|
|
|
359
359
|
* 것은 **판이 다시 끝난 것이 아니다.** 그래서 "몇 번째 결과창인가" 가 아니라
|
|
360
360
|
* **"몇 번째 판인가"** 로 가린다.
|
|
361
361
|
*/
|
|
362
|
-
const
|
|
363
|
-
const
|
|
362
|
+
const roundNoRef = useRef(0);
|
|
363
|
+
const closeResultOverlay = () => { resultGenerationRef.current += 1; roundNoRef.current += 1; setResultOverlay(null); };
|
|
364
364
|
const [noticeMessage, setNoticeMessage] = useState('');
|
|
365
365
|
const [flowState, setFlowState] = useState(() => createScenarioFlowState());
|
|
366
366
|
const prevPageRef = useRef(typeof pageIndex === 'number' ? pageIndex : null);
|
|
@@ -370,7 +370,7 @@ function TrainingPagePopupInner(props) {
|
|
|
370
370
|
if (!hasScenario && !isChildrenMode)
|
|
371
371
|
return;
|
|
372
372
|
setFlowState(createScenarioFlowState());
|
|
373
|
-
|
|
373
|
+
closeResultOverlay();
|
|
374
374
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
375
375
|
}, [hasScenario, scenarioId, isChildrenMode]);
|
|
376
376
|
/* eslint-enable react-hooks/set-state-in-effect */
|
|
@@ -434,14 +434,14 @@ function TrainingPagePopupInner(props) {
|
|
|
434
434
|
const pageId = hasScenario
|
|
435
435
|
? getScenarioPageId(currentScenarioPage, effectivePageIndex)
|
|
436
436
|
: (currentChildrenPage?.id || `children-page-${effectivePageIndex ?? 0}`);
|
|
437
|
-
const
|
|
437
|
+
const nowIso = new Date().toISOString();
|
|
438
438
|
for (let i = 0; i <= effectiveStepIndex; i += 1) {
|
|
439
439
|
const stepKey = hasScenario
|
|
440
440
|
? getScenarioStepKey(currentScenarioSteps[i], i)
|
|
441
441
|
: `step-${i}`;
|
|
442
|
-
const k =
|
|
443
|
-
if (
|
|
444
|
-
|
|
442
|
+
const k = shownAtKey(pageId, stepKey);
|
|
443
|
+
if (!stepShownAt.current.has(k))
|
|
444
|
+
stepShownAt.current.set(k, nowIso);
|
|
445
445
|
}
|
|
446
446
|
/* `currentScenarioSteps` 는 그릴 때마다 새 배열이라 의존성에 넣으면 매번 돈다 —
|
|
447
447
|
쪽·스텝 번호가 바뀔 때만 돌면 되고, 그 둘이 바뀌면 스텝 목록도 함께 바뀐다. */
|
|
@@ -548,8 +548,8 @@ function TrainingPagePopupInner(props) {
|
|
|
548
548
|
* `state` 가 아니라 `ref` 다: 이 값은 **그릴 것을 바꾸지 않는다.** state 로 두면 스텝이
|
|
549
549
|
* 바뀔 때마다 한 번씩 더 그린다.
|
|
550
550
|
*/
|
|
551
|
-
const
|
|
552
|
-
const
|
|
551
|
+
const stepShownAt = useRef(new Map());
|
|
552
|
+
const shownAtKey = (pageId, stepKey) => `${pageId}\u0000${stepKey}`;
|
|
553
553
|
const setScenarioAnswer = useCallback((pageId, stepKey, payload) => {
|
|
554
554
|
setFlowState((prev) => {
|
|
555
555
|
const pageAnswers = prev.answersByPage[pageId] || {};
|
|
@@ -566,14 +566,14 @@ function TrainingPagePopupInner(props) {
|
|
|
566
566
|
- `shownAt` 은 위 `스텝보인때` 에 적어 둔 것을 꺼낸다.
|
|
567
567
|
글자꼴은 `Z` 다 — **여기서 짓는 때는 epoch 하나**라 지킬 지역 시계가 없다
|
|
568
568
|
(앱이 글자로 보낸 때를 안 고치는 것과 다른 자리다, `trainingItemResults` 머리글). */
|
|
569
|
-
const
|
|
569
|
+
const payloadWithTimes = withStepTimes(payload, stepShownAt.current.get(shownAtKey(pageId, stepKey)), incomingTs);
|
|
570
570
|
return {
|
|
571
571
|
...prev,
|
|
572
572
|
answersByPage: {
|
|
573
573
|
...prev.answersByPage,
|
|
574
574
|
[pageId]: {
|
|
575
575
|
...pageAnswers,
|
|
576
|
-
[stepKey]:
|
|
576
|
+
[stepKey]: payloadWithTimes,
|
|
577
577
|
},
|
|
578
578
|
},
|
|
579
579
|
};
|
|
@@ -691,8 +691,8 @@ function TrainingPagePopupInner(props) {
|
|
|
691
691
|
return baseResult;
|
|
692
692
|
};
|
|
693
693
|
const handleViewResults = () => {
|
|
694
|
-
const
|
|
695
|
-
const
|
|
694
|
+
const myResult = ++resultGenerationRef.current;
|
|
695
|
+
const isStale = () => myResult !== resultGenerationRef.current;
|
|
696
696
|
try {
|
|
697
697
|
if (typeof document !== 'undefined' && document.body) {
|
|
698
698
|
try {
|
|
@@ -706,11 +706,11 @@ function TrainingPagePopupInner(props) {
|
|
|
706
706
|
catch (e) { }
|
|
707
707
|
if (hasScenario) {
|
|
708
708
|
const maybe = buildScenarioResult();
|
|
709
|
-
setResultOverlay(
|
|
709
|
+
setResultOverlay(RESULT_PENDING);
|
|
710
710
|
const maybeAny = maybe;
|
|
711
711
|
Promise.resolve(maybeAny)
|
|
712
712
|
.then((data) => {
|
|
713
|
-
if (
|
|
713
|
+
if (isStale())
|
|
714
714
|
return;
|
|
715
715
|
if (data)
|
|
716
716
|
setResultOverlay(data);
|
|
@@ -720,7 +720,7 @@ function TrainingPagePopupInner(props) {
|
|
|
720
720
|
.catch((err) => {
|
|
721
721
|
if (import.meta.env?.DEV)
|
|
722
722
|
console.error('viewResults scenario handler failed', err);
|
|
723
|
-
if (
|
|
723
|
+
if (isStale())
|
|
724
724
|
return;
|
|
725
725
|
setResultOverlay({ isEmpty: true });
|
|
726
726
|
});
|
|
@@ -728,11 +728,11 @@ function TrainingPagePopupInner(props) {
|
|
|
728
728
|
}
|
|
729
729
|
if (isChildrenMode) {
|
|
730
730
|
const maybe = buildChildrenResult();
|
|
731
|
-
setResultOverlay(
|
|
731
|
+
setResultOverlay(RESULT_PENDING);
|
|
732
732
|
const maybeAny = maybe;
|
|
733
733
|
Promise.resolve(maybeAny)
|
|
734
734
|
.then((data) => {
|
|
735
|
-
if (
|
|
735
|
+
if (isStale())
|
|
736
736
|
return;
|
|
737
737
|
if (data)
|
|
738
738
|
setResultOverlay(data);
|
|
@@ -742,7 +742,7 @@ function TrainingPagePopupInner(props) {
|
|
|
742
742
|
.catch((err) => {
|
|
743
743
|
if (import.meta.env?.DEV)
|
|
744
744
|
console.error('viewResults children handler failed', err);
|
|
745
|
-
if (
|
|
745
|
+
if (isStale())
|
|
746
746
|
return;
|
|
747
747
|
setResultOverlay({ isEmpty: true });
|
|
748
748
|
});
|
|
@@ -769,16 +769,16 @@ function TrainingPagePopupInner(props) {
|
|
|
769
769
|
setResultOverlay(maybeAny);
|
|
770
770
|
}
|
|
771
771
|
else {
|
|
772
|
-
setResultOverlay(maybeAny && typeof maybeAny.then === 'function' ?
|
|
772
|
+
setResultOverlay(maybeAny && typeof maybeAny.then === 'function' ? RESULT_PENDING : {});
|
|
773
773
|
}
|
|
774
774
|
// 결과가 비었거나 실패하면 막지 않는다(전과 같이 빈 창) — 계산 중 표시만 걷는다
|
|
775
|
-
const
|
|
776
|
-
setResultOverlay((cur) => (cur ===
|
|
777
|
-
Promise.resolve(maybeAny).then((data) => { if (data &&
|
|
775
|
+
const clearPending = () => { if (!isStale())
|
|
776
|
+
setResultOverlay((cur) => (cur === RESULT_PENDING ? {} : cur)); };
|
|
777
|
+
Promise.resolve(maybeAny).then((data) => { if (data && !isStale())
|
|
778
778
|
setResultOverlay(data);
|
|
779
779
|
else
|
|
780
|
-
|
|
781
|
-
console.error('viewResults child handler failed', err);
|
|
780
|
+
clearPending(); }).catch((err) => { if (import.meta.env?.DEV)
|
|
781
|
+
console.error('viewResults child handler failed', err); clearPending(); });
|
|
782
782
|
return;
|
|
783
783
|
}
|
|
784
784
|
if (typeof externalOnResult === 'function') {
|
|
@@ -793,18 +793,18 @@ function TrainingPagePopupInner(props) {
|
|
|
793
793
|
setResultOverlay(maybeAny);
|
|
794
794
|
}
|
|
795
795
|
else {
|
|
796
|
-
setResultOverlay(maybeAny && typeof maybeAny.then === 'function' ?
|
|
796
|
+
setResultOverlay(maybeAny && typeof maybeAny.then === 'function' ? RESULT_PENDING : {});
|
|
797
797
|
}
|
|
798
|
-
const
|
|
799
|
-
setResultOverlay((cur) => (cur ===
|
|
798
|
+
const clearPending = () => { if (!isStale())
|
|
799
|
+
setResultOverlay((cur) => (cur === RESULT_PENDING ? {} : cur)); };
|
|
800
800
|
Promise.resolve(maybeAny).then((data) => { try {
|
|
801
801
|
if (import.meta && import.meta.env && import.meta.env.DEV)
|
|
802
802
|
console.debug('TrainingPagePopup: externalOnResult resolved data:', data);
|
|
803
803
|
}
|
|
804
|
-
catch (e) { } if (data &&
|
|
804
|
+
catch (e) { } if (data && !isStale())
|
|
805
805
|
setResultOverlay(data);
|
|
806
806
|
else
|
|
807
|
-
|
|
807
|
+
clearPending(); }).catch((err) => { console.error('viewResults external handler failed', err); clearPending(); });
|
|
808
808
|
return;
|
|
809
809
|
}
|
|
810
810
|
};
|
|
@@ -902,26 +902,26 @@ function TrainingPagePopupInner(props) {
|
|
|
902
902
|
};
|
|
903
903
|
/* 다음 문제로 넘어가면 포커스를 새 문제의 첫 칸으로. '다음 문제' 는 누르는 즉시 비활성이 되어 포커스가 body 로 빠졌다 —
|
|
904
904
|
키보드 사용자는 매 문제 판 처음부터 Tab 을 다시 밟았다. 포커스가 판 안 다른 곳에 있으면 건드리지 않는다. */
|
|
905
|
-
const
|
|
905
|
+
const lastPageRef = useRef(undefined);
|
|
906
906
|
useEffect(() => {
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
if (
|
|
907
|
+
const prevPage = lastPageRef.current;
|
|
908
|
+
lastPageRef.current = effectivePageIndex;
|
|
909
|
+
if (prevPage === undefined || prevPage === effectivePageIndex)
|
|
910
910
|
return;
|
|
911
911
|
// 키보드로 넘겼을 때만 — 마우스로 누른 사람에게 매 문제 '듣기' 초점 안내가 뜨지 않게
|
|
912
|
-
if (
|
|
912
|
+
if (!wasKeyboardPress())
|
|
913
913
|
return;
|
|
914
914
|
const panel = panelRef.current;
|
|
915
915
|
if (!panel)
|
|
916
916
|
return;
|
|
917
917
|
const active = document.activeElement;
|
|
918
|
-
const
|
|
919
|
-
if (
|
|
918
|
+
const lostFocus = !active || active === document.body || !panel.contains(active) || active.disabled === true;
|
|
919
|
+
if (!lostFocus)
|
|
920
920
|
return;
|
|
921
|
-
const
|
|
921
|
+
const firstCell = panel.querySelector(`.training-steps ${FOCUSABLE_SELECTOR.split(', ').join(', .training-steps ')}`)
|
|
922
922
|
?? Array.from(panel.querySelectorAll(FOCUSABLE_SELECTOR)).find((el) => !el.closest('[aria-hidden="true"]'));
|
|
923
923
|
try {
|
|
924
|
-
|
|
924
|
+
firstCell?.focus();
|
|
925
925
|
}
|
|
926
926
|
catch (e) { }
|
|
927
927
|
}, [effectivePageIndex]);
|
|
@@ -948,17 +948,17 @@ function TrainingPagePopupInner(props) {
|
|
|
948
948
|
*
|
|
949
949
|
* 세대로 **판마다 한 번**만 적는다(다시하기는 새 판이라 또 적는다).
|
|
950
950
|
*/
|
|
951
|
-
const
|
|
952
|
-
const
|
|
951
|
+
const reportedRoundRef = useRef(-1);
|
|
952
|
+
const reportRoundDone = useCallback(() => {
|
|
953
953
|
if (!typeKey)
|
|
954
954
|
return undefined;
|
|
955
955
|
const ro = resultOverlay;
|
|
956
|
-
const { results, correct: finalCorrect, total } =
|
|
957
|
-
const
|
|
956
|
+
const { results, correct: finalCorrect, total } = countRoundResult(resultOverlay);
|
|
957
|
+
const result = { correct: finalCorrect, total, attempt: previousRoundsRef.current.length + 1, previousAttempts: [...previousRoundsRef.current] };
|
|
958
958
|
/* 이미 이 판을 적었으면 **값만 돌려준다** — 닫을 때 `onComplete` 가 그 값을 받아야 한다 */
|
|
959
|
-
if (
|
|
960
|
-
return
|
|
961
|
-
|
|
959
|
+
if (reportedRoundRef.current === roundNoRef.current)
|
|
960
|
+
return result;
|
|
961
|
+
reportedRoundRef.current = roundNoRef.current;
|
|
962
962
|
/* 문항 하나하나를 함께 남긴다 — 여기서 접으면 어느 낱말을 틀렸는지 되찾을 길이 없다.
|
|
963
963
|
'음소별 오류'(08-26 회의)가 이걸 원재료로 쓴다. */
|
|
964
964
|
logTrainingCompletion({
|
|
@@ -974,39 +974,39 @@ function TrainingPagePopupInner(props) {
|
|
|
974
974
|
dailyTotal: dailyTotal ?? null,
|
|
975
975
|
});
|
|
976
976
|
try {
|
|
977
|
-
onRoundFinished?.(
|
|
977
|
+
onRoundFinished?.(result);
|
|
978
978
|
}
|
|
979
979
|
catch (e) { /* 알리는 쪽의 실패로 적는 일을 막지 않는다 */ }
|
|
980
|
-
return
|
|
980
|
+
return result;
|
|
981
981
|
}, [typeKey, entryPoint, dailyTotal, itemRef, resultOverlay, onRoundFinished]);
|
|
982
982
|
/* **결과창이 서면 적는다** — 닫기를 안 기다린다.
|
|
983
983
|
결과를 세우는 자리가 넷이라(갈래·어린이·자식 손잡이·바깥 손잡이) 그 넷에 각각 적으면
|
|
984
984
|
**한 자리가 안 따라간다.** 세운 값을 보고 여기 한 곳에서 적는다.
|
|
985
985
|
`결과계산중`·빈 결과는 아직 사실이 아니다 — 적지 않는다. */
|
|
986
986
|
useEffect(() => {
|
|
987
|
-
if (!resultOverlay || resultOverlay ===
|
|
987
|
+
if (!resultOverlay || resultOverlay === RESULT_PENDING)
|
|
988
988
|
return;
|
|
989
989
|
if (resultOverlay?.isEmpty)
|
|
990
990
|
return;
|
|
991
991
|
/* **셀 것이 있어야 적는다.** 결과가 `{}` 로 서는 길이 있다(자식이 빈손을 주거나 늦게
|
|
992
992
|
실패했을 때) — 그것을 판이 끝난 것으로 적으면 `0/0` 짜리 기록이 쌓인다.
|
|
993
993
|
그 꼴에서도 **닫을 때는 전과 같이 적힌다**(아래 그물) — 여기서 앞당기지만 않는다. */
|
|
994
|
-
if (!((
|
|
994
|
+
if (!((countRoundResult(resultOverlay).total ?? 0) > 0))
|
|
995
995
|
return;
|
|
996
|
-
|
|
997
|
-
}, [resultOverlay,
|
|
996
|
+
reportRoundDone();
|
|
997
|
+
}, [resultOverlay, reportRoundDone]);
|
|
998
998
|
const handleResultDone = useCallback(() => {
|
|
999
999
|
/* 여기서도 한 번 부른다 — **그물이다.** 위 효과가 못 돈 꼴(결과가 빈 채로 닫는 길 등)에서도
|
|
1000
1000
|
적히게 두되, 이미 적었으면 세대가 막아 **두 번 안 적힌다.** */
|
|
1001
|
-
const completeResult =
|
|
1002
|
-
|
|
1001
|
+
const completeResult = reportRoundDone();
|
|
1002
|
+
closeResultOverlay();
|
|
1003
1003
|
scheduleClose(() => {
|
|
1004
1004
|
if (typeof onComplete === 'function')
|
|
1005
1005
|
onComplete(completeResult);
|
|
1006
1006
|
if (typeof onBack === 'function')
|
|
1007
1007
|
onBack();
|
|
1008
1008
|
});
|
|
1009
|
-
}, [onComplete, onBack,
|
|
1009
|
+
}, [onComplete, onBack, reportRoundDone, setResultOverlay, scheduleClose]);
|
|
1010
1010
|
const handleNoticeClose = useCallback(() => setNoticeMessage(''), [setNoticeMessage]);
|
|
1011
1011
|
const handleCloseBack = useCallback(() => {
|
|
1012
1012
|
scheduleClose(() => { if (typeof onBack === 'function')
|
|
@@ -1027,11 +1027,11 @@ function TrainingPagePopupInner(props) {
|
|
|
1027
1027
|
onReplay: () => {
|
|
1028
1028
|
// 다시 푸는 판의 소요 시간은 지금부터 — 마운트 때 한 번만 찍어 앞 판 시간·결과 창을 보던 시간까지 섞였다(60초가 660초)
|
|
1029
1029
|
completionStartRef.current = Date.now();
|
|
1030
|
-
if (resultOverlay !==
|
|
1031
|
-
const { correct, total } =
|
|
1032
|
-
|
|
1030
|
+
if (resultOverlay !== RESULT_PENDING) {
|
|
1031
|
+
const { correct, total } = countRoundResult(resultOverlay);
|
|
1032
|
+
previousRoundsRef.current.push({ correct, total });
|
|
1033
1033
|
}
|
|
1034
|
-
|
|
1034
|
+
closeResultOverlay();
|
|
1035
1035
|
setReplayCount((n) => n + 1);
|
|
1036
1036
|
if (hasScenario) {
|
|
1037
1037
|
setFlowState(createScenarioFlowState());
|
|
@@ -1055,7 +1055,7 @@ function TrainingPagePopupInner(props) {
|
|
|
1055
1055
|
catch (e) { }
|
|
1056
1056
|
},
|
|
1057
1057
|
onClose: handleResultDone,
|
|
1058
|
-
pending: resultOverlay ===
|
|
1058
|
+
pending: resultOverlay === RESULT_PENDING,
|
|
1059
1059
|
})), (resultOverlay?.isEmpty && (_jsx("div", { className: "page-popup page-popup--result-empty", role: "dialog", "aria-modal": "true", "aria-label": t('training.result.eyebrow', '훈련 완료'), children: _jsxs("div", { className: "page-popup__panel page-popup__panel--center", children: [_jsx("div", { className: "page-popup__scroll", children: _jsx("div", { className: `page-popup__content page-popup__content--center${props.contentFull ? ' page-popup__content--full' : ''}`, children: _jsx("div", { className: "page-popup__empty", children: t('training.result.eyebrow', '훈련 완료') }) }) }), _jsx("div", { className: "page-popup__bottom", children: _jsx(Button, { htmlType: "button", variant: "main", size: 48, onClick: handleResultDone, label: t('training.result.done', '완료') }) })] }) }))), _jsx(Toast, { message: noticeMessage, position: "top", onClose: handleNoticeClose })] }));
|
|
1060
1060
|
const portalContent = (_jsx(NoisePlayerContext.Provider, { value: noiseContextValue, children: jsx }));
|
|
1061
1061
|
if (typeof document !== 'undefined' && document.body)
|
|
@@ -20,9 +20,9 @@ export default function TrainingPagePopupBottom({ useNoiseSettingBtn = false, on
|
|
|
20
20
|
const mobileRef = useRef(null);
|
|
21
21
|
const noisePlayer = useNoisePlayer();
|
|
22
22
|
/* 쪽이 없으면(0) 쪽 이동만 숨기고 끝내는 단추는 둔다 — 전에는 0 === -1 이라 단추가 하나도 없어 판을 끝낼 수 없었다 */
|
|
23
|
-
const
|
|
24
|
-
const useNextBtn =
|
|
25
|
-
const useResultBtn =
|
|
23
|
+
const hasPages = pageCount > 0;
|
|
24
|
+
const useNextBtn = hasPages && pageIndex < pageCount - 1;
|
|
25
|
+
const useResultBtn = !hasPages || pageIndex >= pageCount - 1;
|
|
26
26
|
const isBtnActive = stepIndex === Math.max(0, stepCount);
|
|
27
27
|
// 노이즈 설정 버튼이 비활성화되면 열려 있던 팝업을 닫는 의도된 동기화다
|
|
28
28
|
/* eslint-disable react-hooks/set-state-in-effect */
|
|
@@ -65,35 +65,35 @@ export default function TrainingPagePopupBottom({ useNoiseSettingBtn = false, on
|
|
|
65
65
|
굴려서 꺼낼 수도 없었다(말귀 실측 2026-09-22: 훈련을 이어갈 수 없다). 폰에서는 토글 단추
|
|
66
66
|
**위에 떠서** 보인다(사용자 지시). 데스크톱은 그대로 바닥 줄에 나란히 선다 — 거기서는
|
|
67
67
|
자리가 넉넉하다(1659px 창에서 왼쪽 묶음 434px). */
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const [
|
|
68
|
+
const isPhone = useMediaQuery('(max-width: 767px)');
|
|
69
|
+
const meterToggleRef = useRef(null);
|
|
70
|
+
const [meterRect, setMeterRect] = useState(null);
|
|
71
71
|
const handleToggleMeters = useCallback(() => {
|
|
72
72
|
setShowMeters(prev => {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
return
|
|
73
|
+
const next = !prev;
|
|
74
|
+
setMeterRect(next ? (meterToggleRef.current?.getBoundingClientRect() ?? null) : null);
|
|
75
|
+
return next;
|
|
76
76
|
});
|
|
77
77
|
}, []);
|
|
78
|
-
const
|
|
78
|
+
const closeMeters = useCallback(() => { setShowMeters(false); setMeterRect(null); }, []);
|
|
79
79
|
/* **결과 창이 뜨면 미터를 끈다**(사용자 지시 2026-09-22). 결과를 보는 동안 미터가 떠 있을
|
|
80
80
|
까닭이 없고, 창을 닫고 다음 판을 시작할 때도 깨끗한 자리에서 시작하는 편이 낫다.
|
|
81
81
|
아래 설정 창 동기화와 같은 결이다 — 의도한 동기화라 set-state-in-effect 를 연다. */
|
|
82
82
|
/* eslint-disable-next-line react-hooks/set-state-in-effect */
|
|
83
83
|
useEffect(() => { if (resultOpen) {
|
|
84
84
|
setShowMeters(false);
|
|
85
|
-
|
|
85
|
+
setMeterRect(null);
|
|
86
86
|
} }, [resultOpen]);
|
|
87
|
-
const
|
|
87
|
+
const meters = (_jsxs(_Fragment, { children: [_jsxs("div", { className: "pp-bottom__meter", children: [_jsx("span", { className: "pp-bottom__meter-label caption-1", children: t('training.meter.sentence', '재생') }), _jsx(AudioMeter, { analyser: noisePlayer?.mainAnalyser ?? null, orientation: "horizontal", length: 112, label: "db", labelPos: "left", ariaLabel: t('training.meter.sentence', '재생') })] }), _jsxs("div", { className: "pp-bottom__meter", children: [_jsx("span", { className: "pp-bottom__meter-label caption-1", children: t('training.meter.noise', '소음') }), _jsx(AudioMeter, { analyser: noisePlayer?.noiseAnalyser ?? null, orientation: "horizontal", length: 112, label: "db", labelPos: "left", ariaLabel: t('training.meter.noise', '소음') })] })] }));
|
|
88
88
|
/* 설정 창은 판 밖(body)으로 포털된다 — 열리면 초점을 창 첫 칸으로, 닫히면 연 단추로(useFloatingFocus 참고) */
|
|
89
|
-
const
|
|
90
|
-
useFloatingFocus(
|
|
91
|
-
const
|
|
89
|
+
const panelOpen = useNoiseSettingBtn && open;
|
|
90
|
+
useFloatingFocus(panelOpen, () => (typeof document !== 'undefined' ? document.querySelector('.noise-settings__header')?.closest('.tooltip-content') ?? null : null), () => {
|
|
91
|
+
const visibleButtons = [desktopRef.current, mobileRef.current]
|
|
92
92
|
.map((w) => w?.querySelector('button') ?? null)
|
|
93
93
|
.find((b) => !!b && b.getClientRects().length > 0);
|
|
94
|
-
return
|
|
94
|
+
return visibleButtons ?? desktopRef.current?.querySelector('button') ?? null;
|
|
95
95
|
});
|
|
96
96
|
return (_jsxs("div", { className: "pp-bottom", children: [_jsxs("div", { className: "pp-bottom__left", children: [useNoiseSettingBtn ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "pp-bottom__settings--desktop", children: _jsx("div", { ref: desktopRef, className: "pp-bottom__tooltip-trigger", "data-tooltip-ignore": true, children: _jsx(Button, { htmlType: "button", size: 56, type: "bar", icon: 'settings', onClick: handleDesktopClick,
|
|
97
97
|
/* 여닫이 — aria-pressed(selected) 대신 aria-expanded. 보이는 선택 모양은 같은 클래스로 */
|
|
98
|
-
className: open ? 'is-selected' : undefined, "aria-expanded": open, "aria-haspopup": "dialog", ariaLabel: t('training.trainingSettingsTitle', '훈련 설정'), label: t('training.trainingSettingsTitle', '훈련 설정'), minWidth: 140 }) }) }), _jsx("div", { className: "pp-bottom__settings--mobile", children: _jsx("div", { ref: mobileRef, className: "pp-bottom__tooltip-trigger", "data-tooltip-ignore": true, children: _jsx(Button, { htmlType: "button", size: 56, type: "dot", shape: "pill", icon: 'settings', onClick: handleMobileClick, className: open ? 'is-selected' : undefined, "aria-expanded": open, "aria-haspopup": "dialog", ariaLabel: t('training.settings', '설정') }) }) })] })) : null, useNoiseSettingBtn && (_jsx("div", { ref:
|
|
98
|
+
className: open ? 'is-selected' : undefined, "aria-expanded": open, "aria-haspopup": "dialog", ariaLabel: t('training.trainingSettingsTitle', '훈련 설정'), label: t('training.trainingSettingsTitle', '훈련 설정'), minWidth: 140 }) }) }), _jsx("div", { className: "pp-bottom__settings--mobile", children: _jsx("div", { ref: mobileRef, className: "pp-bottom__tooltip-trigger", "data-tooltip-ignore": true, children: _jsx(Button, { htmlType: "button", size: 56, type: "dot", shape: "pill", icon: 'settings', onClick: handleMobileClick, className: open ? 'is-selected' : undefined, "aria-expanded": open, "aria-haspopup": "dialog", ariaLabel: t('training.settings', '설정') }) }) })] })) : null, useNoiseSettingBtn && (_jsx("div", { ref: meterToggleRef, className: "pp-bottom__meter-trigger", "data-tooltip-ignore": true, children: _jsx(Button, { htmlType: "button", size: 40, type: "dot", shape: "pill", icon: "audio-lines", onClick: handleToggleMeters, selected: showMeters, "aria-expanded": isPhone ? showMeters : undefined, ariaLabel: t('training.meter.toggle', '오디오 미터 표시') }) })), useNoiseSettingBtn && showMeters && !isPhone && (_jsx("div", { className: "pp-bottom__meters", children: meters }))] }), useNoiseSettingBtn && showMeters && isPhone && meterRect && (_jsx(Tooltip, { anchorRect: meterRect, onClose: closeMeters, ariaLabel: t('training.meter.toggle', '오디오 미터 표시'), children: _jsx("div", { className: "pp-bottom__meters pp-bottom__meters--floating", children: meters }) })), _jsxs("div", { className: "pp-bottom__right", children: [useResultBtn && (_jsx(Button, { htmlType: "button", size: 56, type: "bar", icon: "check", onClick: typeof onResult === "function" ? onResult : onNext, ariaLabel: t('training.viewResult', '결과보기'), label: t('training.viewResult', '결과보기'), variant: "main", disabled: !isBtnActive })), useNextBtn && (_jsx(Button, { htmlType: "button", size: 56, type: "bar", rightIcon: "chevron-right", align: "right", onClick: onNext, ariaLabel: t('training.nextQuestion', '다음 문제'), label: t('training.nextQuestion', '다음 문제'), variant: "main", disabled: !isBtnActive }))] }), useNoiseSettingBtn && open && (_jsx(NoiseSettingsTooltip, { anchorRect: anchorRect, onClose: handleCloseTooltip, hideNoiseRows: hideNoiseRows }))] }));
|
|
99
99
|
}
|