@ihabdevteam/core 0.150.0 → 0.152.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 +71 -0
- package/dist/components.cjs.js +199 -186
- package/dist/components.esm.css +5 -5
- package/dist/hooks.cjs.js +5 -1
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core-tokens.css +1 -1
- package/dist/ihabdevteam-core.cjs.js +200 -187
- package/dist/ihabdevteam-core.css +1 -1
- package/dist/ihabdevteam-core.d.ts +405 -67
- package/dist/packages/core/src/components/Accordion.d.ts +6 -0
- package/dist/packages/core/src/components/Accordion.js +5 -2
- package/dist/packages/core/src/components/Alert.d.ts +7 -1
- package/dist/packages/core/src/components/Alert.js +12 -3
- package/dist/packages/core/src/components/BarChart.js +7 -2
- package/dist/packages/core/src/components/Board.d.ts +5 -2
- package/dist/packages/core/src/components/Board.js +9 -3
- package/dist/packages/core/src/components/BottomNav.js +5 -4
- package/dist/packages/core/src/components/Breadcrumb.css +9 -0
- package/dist/packages/core/src/components/Breadcrumb.d.ts +12 -1
- package/dist/packages/core/src/components/Breadcrumb.js +8 -2
- package/dist/packages/core/src/components/BulkBar.css +2 -0
- package/dist/packages/core/src/components/BulkBar.js +21 -2
- package/dist/packages/core/src/components/Button.d.ts +12 -2
- package/dist/packages/core/src/components/Button.js +70 -13
- package/dist/packages/core/src/components/Calendar.d.ts +6 -1
- package/dist/packages/core/src/components/Calendar.js +3 -3
- package/dist/packages/core/src/components/ChartSection.css +11 -4
- package/dist/packages/core/src/components/DatePicker.d.ts +7 -1
- package/dist/packages/core/src/components/DatePicker.js +25 -15
- package/dist/packages/core/src/components/Dropdown.d.ts +1 -0
- package/dist/packages/core/src/components/Dropdown.js +7 -5
- package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.d.ts +12 -1
- package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.js +25 -8
- package/dist/packages/core/src/components/EmptyState.d.ts +5 -1
- package/dist/packages/core/src/components/EmptyState.js +4 -1
- package/dist/packages/core/src/components/ErrorState.d.ts +5 -2
- package/dist/packages/core/src/components/ErrorState.js +3 -1
- package/dist/packages/core/src/components/FileUpload.d.ts +9 -1
- package/dist/packages/core/src/components/FileUpload.js +8 -2
- package/dist/packages/core/src/components/FilterSectionInfo.js +14 -2
- package/dist/packages/core/src/components/Header.js +8 -5
- package/dist/packages/core/src/components/HearingDeviceInfo.js +2 -1
- package/dist/packages/core/src/components/Icon.d.ts +2 -1
- package/dist/packages/core/src/components/Icon.js +4 -1
- package/dist/packages/core/src/components/Loader.css +28 -0
- package/dist/packages/core/src/components/Loader.d.ts +5 -1
- package/dist/packages/core/src/components/Loader.js +4 -2
- package/dist/packages/core/src/components/NoticeBox.d.ts +1 -1
- package/dist/packages/core/src/components/NoticeBox.js +15 -6
- package/dist/packages/core/src/components/PanelRow.js +2 -1
- package/dist/packages/core/src/components/PassCard/PassCard.d.ts +8 -2
- package/dist/packages/core/src/components/PassCard/PassCard.js +25 -10
- package/dist/packages/core/src/components/PassCard/passCardProduct.d.ts +1 -1
- package/dist/packages/core/src/components/PassCard/passCardProduct.js +11 -2
- package/dist/packages/core/src/components/Popup/ConfirmPopup.d.ts +11 -2
- package/dist/packages/core/src/components/Popup/ConfirmPopup.js +18 -3
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +10 -8
- package/dist/packages/core/src/components/Popup/ListPopup.js +6 -6
- package/dist/packages/core/src/components/Popup/LoadTablePopup.css +7 -0
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +27 -5
- package/dist/packages/core/src/components/Popup/Popup.d.ts +8 -0
- package/dist/packages/core/src/components/Popup/Popup.js +6 -3
- package/dist/packages/core/src/components/Popup/SaveTablePopup.d.ts +5 -1
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.css +3 -0
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.d.ts +5 -0
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +10 -4
- package/dist/packages/core/src/components/Popup/WordDataUpdatePopup.css +9 -0
- package/dist/packages/core/src/components/ProgressIndicator.css +5 -0
- package/dist/packages/core/src/components/ProgressIndicator.d.ts +3 -1
- package/dist/packages/core/src/components/ProgressIndicator.js +5 -3
- package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.d.ts +4 -1
- package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +27 -15
- package/dist/packages/core/src/components/Rating.d.ts +8 -2
- package/dist/packages/core/src/components/Rating.js +22 -8
- package/dist/packages/core/src/components/Report/ReportDocument.d.ts +10 -7
- package/dist/packages/core/src/components/Report/ReportDocument.js +39 -13
- package/dist/packages/core/src/components/RingChart.d.ts +1 -1
- package/dist/packages/core/src/components/RingChart.js +1 -1
- package/dist/packages/core/src/components/SegmentedControl.d.ts +4 -0
- package/dist/packages/core/src/components/SegmentedControl.js +9 -1
- package/dist/packages/core/src/components/ShortcutBadge.css +16 -1
- package/dist/packages/core/src/components/ShortcutBadge.d.ts +1 -0
- package/dist/packages/core/src/components/SideNav.js +8 -2
- package/dist/packages/core/src/components/SummaryStrip.d.ts +1 -1
- package/dist/packages/core/src/components/SummaryStrip.js +2 -2
- package/dist/packages/core/src/components/Table.d.ts +1 -1
- package/dist/packages/core/src/components/Table.js +4 -1
- package/dist/packages/core/src/components/Tabs.d.ts +1 -0
- package/dist/packages/core/src/components/Tabs.js +4 -1
- package/dist/packages/core/src/components/TextField.css +5 -6
- package/dist/packages/core/src/components/TextField.js +46 -3
- package/dist/packages/core/src/components/Toast.js +16 -2
- package/dist/packages/core/src/components/Tooltip/Tooltip.css +3 -1
- package/dist/packages/core/src/components/Tooltip/Tooltip.js +5 -2
- package/dist/packages/core/src/components/Tooltip/tooltipConstants.d.ts +10 -2
- package/dist/packages/core/src/components/Tooltip/tooltipConstants.js +10 -2
- package/dist/packages/core/src/components/TopBanner.d.ts +1 -1
- package/dist/packages/core/src/components/TopBanner.js +66 -8
- package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.d.ts +1 -0
- package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +65 -15
- package/dist/packages/core/src/components/Training/NoisePlayerContext.d.ts +3 -2
- package/dist/packages/core/src/components/Training/NoisePlayerContext.js +10 -4
- package/dist/packages/core/src/components/Training/TrainingBlockButton.css +7 -0
- package/dist/packages/core/src/components/Training/TrainingBlockButton.js +3 -1
- package/dist/packages/core/src/components/Training/TrainingPagePopup.d.ts +18 -5
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +93 -28
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.d.ts +1 -0
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +16 -3
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +25 -9
- package/dist/packages/core/src/components/Training/TrainingStepCard.js +19 -3
- package/dist/packages/core/src/components/Training/TrainingStepTitle.js +15 -1
- package/dist/packages/core/src/components/Training/TrainingWordCard.css +16 -0
- package/dist/packages/core/src/components/Training/TrainingWordCard.d.ts +6 -0
- package/dist/packages/core/src/components/Training/TrainingWordCard.js +7 -2
- package/dist/packages/core/src/components/Training/trainingBlockButtonTypes.d.ts +2 -2
- package/dist/packages/core/src/components/Training/trainingMissStats.d.ts +10 -0
- package/dist/packages/core/src/components/Training/trainingMissStats.js +19 -1
- package/dist/packages/core/src/components/Training/useFloatingFocus.d.ts +13 -0
- package/dist/packages/core/src/components/Training/useFloatingFocus.js +59 -0
- package/dist/packages/core/src/components/Training/useNoiseFade.js +20 -11
- package/dist/packages/core/src/components/Training/useProgressTicker.d.ts +0 -18
- package/dist/packages/core/src/components/Training/useProgressTicker.js +59 -14
- package/dist/packages/core/src/components/Training/useTTSPlayback.d.ts +1 -1
- package/dist/packages/core/src/components/Training/useTTSPlayback.js +56 -25
- package/dist/packages/core/src/components/Training/useWaveReady.d.ts +4 -1
- package/dist/packages/core/src/components/Training/useWaveReady.js +17 -8
- package/dist/packages/core/src/components/UpdateAlert.css +4 -2
- package/dist/packages/core/src/components/UpdateAlert.d.ts +3 -1
- package/dist/packages/core/src/components/UpdateAlert.js +21 -1
- package/dist/packages/core/src/components/Word/PhonemeSelectBlock.d.ts +2 -0
- package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +12 -4
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.d.ts +6 -0
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +26 -13
- package/dist/packages/core/src/components/Word/WordTableBlock.js +24 -21
- package/dist/packages/core/src/components/Word/WordTableThumbnail.js +1 -1
- package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -10
- package/dist/packages/core/src/components/Word/useWordTablePrint.d.ts +1 -1
- package/dist/packages/core/src/components/Word/useWordTablePrint.js +9 -3
- package/dist/packages/core/src/components/Word/wordScoreStats.d.ts +14 -1
- package/dist/packages/core/src/components/Word/wordScoreStats.js +7 -3
- package/dist/packages/core/src/components/Word/wordTablePrint.js +15 -3
- package/dist/packages/core/src/components/Word/wordTableReview.d.ts +58 -1
- package/dist/packages/core/src/components/Word/wordTableReview.js +75 -17
- package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +9 -0
- package/dist/packages/core/src/components/Word/wordTableSettings.js +11 -0
- package/dist/packages/core/src/components/WordTable.css +10 -0
- package/dist/packages/core/src/components/WordTable.js +39 -26
- package/dist/packages/core/src/components/barChartTypes.d.ts +2 -2
- package/dist/packages/core/src/components/buttonTypes.d.ts +8 -0
- package/dist/packages/core/src/components/updateAlertStack.d.ts +2 -0
- package/dist/packages/core/src/components/updateAlertStack.js +68 -0
- package/dist/packages/core/src/components/useCalendarMonth.d.ts +4 -1
- package/dist/packages/core/src/components/useCalendarMonth.js +6 -3
- package/dist/packages/core/src/components/useComboboxSuggestions.d.ts +7 -0
- package/dist/packages/core/src/components/useComboboxSuggestions.js +21 -2
- package/dist/packages/core/src/components/wordTableLayout.d.ts +4 -0
- package/dist/packages/core/src/components/wordTableLayout.js +6 -0
- package/dist/packages/core/src/components.d.ts +2 -1
- package/dist/packages/core/src/components.js +1 -1
- package/dist/packages/core/src/hooks/bodyScrollLock.js +8 -2
- package/dist/packages/core/src/hooks/focusReturn.d.ts +12 -0
- package/dist/packages/core/src/hooks/focusReturn.js +49 -0
- package/dist/packages/core/src/hooks/touchToggle.d.ts +22 -0
- package/dist/packages/core/src/hooks/touchToggle.js +114 -0
- package/dist/packages/core/src/hooks/useAnchoredMenu.d.ts +12 -3
- package/dist/packages/core/src/hooks/useAnchoredMenu.js +37 -3
- package/dist/packages/core/src/hooks/useDailyTrainingSelection.d.ts +4 -2
- package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +23 -11
- package/dist/packages/core/src/hooks/useIcon.js +29 -5
- package/dist/packages/core/src/hooks/useLucideIcon.d.ts +4 -0
- package/dist/packages/core/src/hooks/useLucideIcon.js +4 -0
- package/dist/packages/core/src/hooks/usePendingPress.d.ts +9 -0
- package/dist/packages/core/src/hooks/usePendingPress.js +26 -0
- package/dist/packages/core/src/hooks/usePrefersReducedMotion.d.ts +15 -0
- package/dist/packages/core/src/hooks/usePrefersReducedMotion.js +26 -0
- package/dist/packages/core/src/hooks/useTablePaging.d.ts +27 -2
- package/dist/packages/core/src/hooks/useTablePaging.js +37 -5
- package/dist/packages/core/src/hooks/useWordAttributeFilter.d.ts +7 -0
- package/dist/packages/core/src/hooks/useWordAttributeFilter.js +25 -0
- package/dist/packages/core/src/i18n/index.js +9 -1
- package/dist/packages/core/src/i18n/locales/en.json +24 -2
- package/dist/packages/core/src/i18n/locales/ko.json +24 -2
- package/dist/packages/core/src/tokens.css +5 -0
- package/dist/packages/core/src/types/wordSearch.d.ts +12 -0
- package/dist/packages/core/src/utils/csv.d.ts +7 -2
- package/dist/packages/core/src/utils/csv.js +12 -5
- package/dist/packages/core/src/utils/dailySelectionUtils.js +44 -14
- package/dist/packages/core/src/utils/date.d.ts +8 -0
- package/dist/packages/core/src/utils/date.js +44 -10
- package/dist/packages/core/src/utils/format.d.ts +1 -1
- package/dist/packages/core/src/utils/format.js +6 -7
- package/dist/packages/core/src/utils/noiseSettingsStore.d.ts +7 -3
- package/dist/packages/core/src/utils/phonemeConvert.d.ts +9 -0
- package/dist/packages/core/src/utils/phonemeConvert.js +11 -0
- package/dist/packages/core/src/utils/phonemePairSearch.d.ts +7 -1
- package/dist/packages/core/src/utils/phonemePairSearch.js +45 -21
- package/dist/packages/core/src/utils/safeHref.d.ts +8 -0
- package/dist/packages/core/src/utils/safeHref.js +24 -0
- package/dist/packages/core/src/utils/savedWordTables.d.ts +19 -3
- package/dist/packages/core/src/utils/savedWordTables.js +27 -19
- package/dist/packages/core/src/utils/textSort.d.ts +9 -0
- package/dist/packages/core/src/utils/textSort.js +37 -1
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +31 -0
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +163 -0
- package/dist/packages/core/src/utils/wordContrast.d.ts +4 -2
- package/dist/packages/core/src/utils/wordContrast.js +8 -2
- package/dist/packages/core/src/utils/wordTrainingGenerators.d.ts +5 -2
- package/dist/packages/core/src/utils/wordTrainingGenerators.js +52 -2
- package/dist/packages/core/src/utils/wordTrainingPool.js +2 -2
- package/dist/packages/core/src/utils/wordTrainingRuntime.js +15 -7
- package/dist/packages/core/src/utils/wordTrainingRuntimeTypes.d.ts +1 -1
- package/dist/packages/core/src/utils/wordTrainingRuntimeTypes.js +5 -4
- package/dist/packages/core/src/utils/wornDate.d.ts +12 -0
- package/dist/packages/core/src/utils/wornDate.js +15 -0
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +2 -2
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useRef } from 'react';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { useToastLifecycle } from './useToastLifecycle';
|
|
6
6
|
import './Toast.css';
|
|
@@ -12,6 +12,20 @@ onClose = () => { }, duration = 2000, // 0 = persistent until message cleared
|
|
|
12
12
|
/* 톡(터치)은 멈추지 않는다 — 톡하면 호환 mouseenter 로 멈추고 mouseleave 는 다른 곳을 톡하기 전까지 안 와 알림이 사라지지 않았다.
|
|
13
13
|
마우스 머무름·키보드 포커스는 그대로 멈춘다. 마지막 포인터 종류를 pointerenter 로 기억해 가른다. */
|
|
14
14
|
const 마지막포인터 = useRef(null);
|
|
15
|
+
/* **첫 문구가 안 읽혔다** — live 영역(role=status)이 문구와 한꺼번에 문서에 들어오면 낭독기는 "바뀐 것" 으로 보지 않는다.
|
|
16
|
+
영역을 먼저 붙이고, 다음 틱(등장 애니메이션이 시작되는 두 프레임 뒤)에 문구를 접근 트리에 넣는다.
|
|
17
|
+
문구 노드는 처음부터 두고 `aria-hidden` 만 푼다 — 글자를 늦게 넣으면 문구를 곧바로 찾는 시험(getByText·status 의
|
|
18
|
+
textContent)이 깨지고, 늘 비어 있는 status 영역을 따로 두면 getByRole('status') 가 둘을 찾는다. 이미 떠 있는 동안 바뀐 문구는 곧바로 읽힌다. */
|
|
19
|
+
const [읽힘, set읽힘] = useState(false);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
// 등장(showing)을 따라 한 번 켜고, 내려가면 끈다 — 외부(애니메이션 수명) 상태와의 의도된 동기화
|
|
22
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
23
|
+
if (showing)
|
|
24
|
+
set읽힘(true);
|
|
25
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
26
|
+
else if (!mounted)
|
|
27
|
+
set읽힘(false);
|
|
28
|
+
}, [showing, mounted]);
|
|
15
29
|
if (!mounted)
|
|
16
30
|
return null;
|
|
17
31
|
const normalizedType = type === 'notice' || type === 'warning' ? type : 'normal';
|
|
@@ -22,7 +36,7 @@ onClose = () => { }, duration = 2000, // 0 = persistent until message cleared
|
|
|
22
36
|
쪽이 미리 정하는 것이고, 읽는 사람이 **그 자리에서** 멈출 수는 없었다.
|
|
23
37
|
다시 움직일 때는 **남은 시간부터** 돈다 — 처음부터가 아니다. */
|
|
24
38
|
_jsx("div", { ref: popupRef, className: `toast toast--${position} ${showing ? 'is-visible' : ''}`, role: "status", "aria-live": "polite", onPointerEnter: (e) => { 마지막포인터.current = e.pointerType; }, onMouseEnter: () => { if (마지막포인터.current !== 'touch')
|
|
25
|
-
pause(); }, onMouseLeave: resume, onFocusCapture: pause, onBlurCapture: resume, children: _jsx("div", { className: `toast__inner toast__inner--${normalizedType} body-2`, children: message }) }));
|
|
39
|
+
pause(); }, onMouseLeave: resume, onFocusCapture: pause, onBlurCapture: resume, children: _jsx("div", { className: `toast__inner toast__inner--${normalizedType} body-2`, "aria-hidden": 읽힘 || showing ? undefined : true, children: message }) }));
|
|
26
40
|
if (typeof document !== 'undefined') {
|
|
27
41
|
return createPortal(el, document.body);
|
|
28
42
|
}
|
|
@@ -42,9 +42,11 @@
|
|
|
42
42
|
padding: 0 2px 10px;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/* 닫힘 페이드 — **애니메이션으로** 준다. 열림 애니메이션(tooltip-pop, fill-mode both)이 끝난 값(opacity 1)을 붙들고 있어
|
|
46
|
+
opacity·transition 선언은 애니메이션 값에 져서 200ms 동안 그대로 보이다 툭 사라졌다. 같은 animation 속성을 바꿔 끼운다. */
|
|
45
47
|
.tooltip-content.is-closing {
|
|
46
48
|
opacity: 0;
|
|
47
|
-
|
|
49
|
+
animation: tooltip-fade-out 200ms ease both;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
/* Placement-specific initial offsets (used by open animation) */
|
|
@@ -5,6 +5,7 @@ import { createPortal } from 'react-dom';
|
|
|
5
5
|
import './Tooltip.css';
|
|
6
6
|
import { FOCUSABLE_SELECTOR } from '../../hooks/useFocusTrap';
|
|
7
7
|
import { pushEsc } from '../../hooks/escStack';
|
|
8
|
+
import { usePrefersReducedMotion } from '../../hooks/usePrefersReducedMotion';
|
|
8
9
|
/** @public */
|
|
9
10
|
export default function Tooltip(props) {
|
|
10
11
|
const { anchorRect, anchorEl, offset = 20, edgeMargin = 20, onClose, closeOnOutsideClick = true, className = '', role = 'dialog', ariaLabel = '', children, backdrop = false, isMobilePopup = false, useBorder = true, header, } = props || {};
|
|
@@ -202,6 +203,8 @@ export default function Tooltip(props) {
|
|
|
202
203
|
// 불렸다. ref 로 "이미 닫는 중"을 즉시 표시해 한 번만 지나가게 한다(Popup 과 같은 방식).
|
|
203
204
|
const closingRef = useRef(false);
|
|
204
205
|
const 닫기타이머Ref = useRef(null);
|
|
206
|
+
/* '동작 줄이기' 면 닫기 대기를 0 으로(페이드가 없으니 기다릴 까닭도 없다) */
|
|
207
|
+
const 동작줄이기 = usePrefersReducedMotion();
|
|
205
208
|
const startClose = useCallback((immediate = false) => {
|
|
206
209
|
/* 닫기를 넘길 곳(onClose)이 없으면 아무것도 하지 않는다. 예전에는 그래도 is-closing 을
|
|
207
210
|
붙여, 부모가 치우지 않는 툴팁이 **흐려진 채 사라진 것처럼** 그 자리에 남았다
|
|
@@ -237,8 +240,8 @@ export default function Tooltip(props) {
|
|
|
237
240
|
closingRef.current = false;
|
|
238
241
|
setClosing(false);
|
|
239
242
|
}
|
|
240
|
-
}, 200);
|
|
241
|
-
}, [closing, onClose, setClosing]);
|
|
243
|
+
}, 동작줄이기 ? 0 : 200);
|
|
244
|
+
}, [closing, onClose, setClosing, 동작줄이기]);
|
|
242
245
|
useEffect(() => () => { if (닫기타이머Ref.current)
|
|
243
246
|
clearTimeout(닫기타이머Ref.current); }, []);
|
|
244
247
|
useEffect(() => {
|
|
@@ -15,12 +15,20 @@ export declare function getNoiseTypeOptions(t: TFunction): {
|
|
|
15
15
|
value: string;
|
|
16
16
|
icon: string;
|
|
17
17
|
}[];
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* @deprecated 모듈을 처음 읽을 때의 언어로 **한 번만** 만든 라벨이라 언어를 바꿔도 따라가지 않는다
|
|
20
|
+
* (서버에서는 늘 기본 언어). `getNoiseLevelOptions(t)` 를 렌더 안에서 `useTranslation()` 의 t 로 부른다.
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
19
23
|
export declare const NOISE_LEVEL_OPTIONS: {
|
|
20
24
|
label: string;
|
|
21
25
|
value: number;
|
|
22
26
|
}[];
|
|
23
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* @deprecated 모듈을 처음 읽을 때의 언어로 **한 번만** 만든 라벨이라 언어를 바꿔도 따라가지 않는다
|
|
29
|
+
* (서버에서는 늘 기본 언어). `getNoiseTypeOptions(t)` 를 렌더 안에서 `useTranslation()` 의 t 로 부른다.
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
24
32
|
export declare const NOISE_TYPE_OPTIONS: {
|
|
25
33
|
label: string;
|
|
26
34
|
value: string;
|
|
@@ -27,9 +27,17 @@ export function getNoiseTypeOptions(t) {
|
|
|
27
27
|
}
|
|
28
28
|
// Backwards-compat static exports (uses i18n singleton — labels won't react to runtime language changes)
|
|
29
29
|
import i18n from '../../i18n';
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* @deprecated 모듈을 처음 읽을 때의 언어로 **한 번만** 만든 라벨이라 언어를 바꿔도 따라가지 않는다
|
|
32
|
+
* (서버에서는 늘 기본 언어). `getNoiseLevelOptions(t)` 를 렌더 안에서 `useTranslation()` 의 t 로 부른다.
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
31
35
|
export const NOISE_LEVEL_OPTIONS = getNoiseLevelOptions(i18n.t.bind(i18n));
|
|
32
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* @deprecated 모듈을 처음 읽을 때의 언어로 **한 번만** 만든 라벨이라 언어를 바꿔도 따라가지 않는다
|
|
38
|
+
* (서버에서는 늘 기본 언어). `getNoiseTypeOptions(t)` 를 렌더 안에서 `useTranslation()` 의 t 로 부른다.
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
33
41
|
export const NOISE_TYPE_OPTIONS = getNoiseTypeOptions(i18n.t.bind(i18n));
|
|
34
42
|
export default {
|
|
35
43
|
PLAYBACK_SPEED_OPTIONS,
|
|
@@ -4,7 +4,7 @@ import './TopBanner.css';
|
|
|
4
4
|
export interface TopBannerProps {
|
|
5
5
|
/** 로테이션할 슬라이드들 — 각 자식이 슬라이드 한 장 */
|
|
6
6
|
children: ReactNode;
|
|
7
|
-
/** 자동 롤링 간격(ms). `0`이면 자동 롤링 안
|
|
7
|
+
/** 자동 롤링 간격(ms). `0`이면 자동 롤링 안 함. OS '동작 줄이기' 가 켜져 있어도 자동 롤링하지 않는다(점으로 넘긴다). */
|
|
8
8
|
interval?: number;
|
|
9
9
|
/**
|
|
10
10
|
* 슬라이드별 인디케이터 톤. 슬라이드 배경이 어두우면 `'light'`(흰 점),
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { cx } from '../utils/cx';
|
|
4
|
-
import { Children, useEffect, useState } from 'react';
|
|
4
|
+
import { Children, useEffect, useRef, useState } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { usePrefersReducedMotion } from '../hooks/usePrefersReducedMotion';
|
|
6
7
|
import './TopBanner.css';
|
|
7
8
|
/**
|
|
8
9
|
* 페이지 상단 롤링 배너 컨테이너.
|
|
@@ -23,18 +24,75 @@ export default function TopBanner({ children, interval = 5000, dotTone, classNam
|
|
|
23
24
|
inert 가 되고 포커스가 body 로 떨어졌다(반대로 마우스 아래에서 포커스만 나가도 넘어갔다) */
|
|
24
25
|
const [hovered, setHovered] = useState(false);
|
|
25
26
|
const [focused, setFocused] = useState(false);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
27
|
+
/* 터치 — 누르는 동안과 뗀 뒤 5초는 멈춘다(배너 밖을 톡하면 곧바로 다시 돈다). 전에는 톡의 호환 mouseenter 로 멈추고 mouseleave 가
|
|
28
|
+
안 와 영영 멈추거나, 누르고 읽는 사이 장이 넘어갔다. 마지막 포인터가 터치면 mouseenter 는 멈춤으로 치지 않는다. */
|
|
29
|
+
const [touched, setTouched] = useState(false);
|
|
30
|
+
const 마지막포인터 = useRef(null);
|
|
31
|
+
const 터치타이머 = useRef(null);
|
|
32
|
+
const sectionRef = useRef(null);
|
|
33
|
+
/* '동작 줄이기' — 저절로 넘어가는 장은 움직임이고 읽는 사이 내용이 바뀐다(WCAG 2.2.2). 자동 롤링을 끈다 */
|
|
34
|
+
const 동작줄이기 = usePrefersReducedMotion();
|
|
35
|
+
const paused = hovered || focused || touched;
|
|
36
|
+
/* 슬라이드 수가 줄면 **지금 장을 범위 안으로 조인다** — `index % count` 는 3장 중 셋째(2)를 보다 2장이 되면 첫째(0)로 튀었다 */
|
|
37
|
+
const current = count > 0 ? Math.min(index, count - 1) : 0;
|
|
29
38
|
useEffect(() => {
|
|
30
|
-
if (count < 2 || interval <= 0 || paused)
|
|
39
|
+
if (count < 2 || interval <= 0 || paused || 동작줄이기)
|
|
31
40
|
return;
|
|
32
|
-
const id = window.setInterval(() => setIndex(i => (i + 1) % count), interval);
|
|
41
|
+
const id = window.setInterval(() => setIndex(i => (Math.min(i, count - 1) + 1) % count), interval);
|
|
33
42
|
return () => window.clearInterval(id);
|
|
34
|
-
}, [count, interval, paused]);
|
|
43
|
+
}, [count, interval, paused, 동작줄이기]);
|
|
44
|
+
const 터치재개예약 = (ms) => {
|
|
45
|
+
if (터치타이머.current != null)
|
|
46
|
+
window.clearTimeout(터치타이머.current);
|
|
47
|
+
터치타이머.current = window.setTimeout(() => { 터치타이머.current = null; setTouched(false); }, ms);
|
|
48
|
+
};
|
|
49
|
+
useEffect(() => () => { if (터치타이머.current != null)
|
|
50
|
+
window.clearTimeout(터치타이머.current); }, []);
|
|
51
|
+
/* 배너 밖을 톡하면 곧바로 다시 돈다 */
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!touched)
|
|
54
|
+
return;
|
|
55
|
+
const 밖톡 = (e) => {
|
|
56
|
+
if (e.pointerType !== 'touch')
|
|
57
|
+
return;
|
|
58
|
+
if (sectionRef.current && sectionRef.current.contains(e.target))
|
|
59
|
+
return;
|
|
60
|
+
if (터치타이머.current != null) {
|
|
61
|
+
window.clearTimeout(터치타이머.current);
|
|
62
|
+
터치타이머.current = null;
|
|
63
|
+
}
|
|
64
|
+
setTouched(false);
|
|
65
|
+
};
|
|
66
|
+
document.addEventListener('pointerdown', 밖톡, true);
|
|
67
|
+
return () => document.removeEventListener('pointerdown', 밖톡, true);
|
|
68
|
+
}, [touched]);
|
|
35
69
|
if (count === 0)
|
|
36
70
|
return null;
|
|
37
|
-
return (_jsxs("section", { className: cx('top-banner', className), "aria-label": ariaLabel, "aria-roledescription": "carousel",
|
|
71
|
+
return (_jsxs("section", { ref: sectionRef, className: cx('top-banner', className), "aria-label": ariaLabel, "aria-roledescription": "carousel", onPointerEnter: (e) => { 마지막포인터.current = e.pointerType; }, onPointerDown: (e) => {
|
|
72
|
+
마지막포인터.current = e.pointerType;
|
|
73
|
+
if (e.pointerType !== 'touch')
|
|
74
|
+
return;
|
|
75
|
+
if (터치타이머.current != null) {
|
|
76
|
+
window.clearTimeout(터치타이머.current);
|
|
77
|
+
터치타이머.current = null;
|
|
78
|
+
}
|
|
79
|
+
setTouched(true);
|
|
80
|
+
}, onPointerUp: (e) => { if (e.pointerType === 'touch')
|
|
81
|
+
터치재개예약(5000); }, onPointerCancel: (e) => { if (e.pointerType === 'touch')
|
|
82
|
+
터치재개예약(5000); }, onMouseEnter: () => { if (마지막포인터.current !== 'touch')
|
|
83
|
+
setHovered(true); }, onMouseLeave: () => setHovered(false),
|
|
84
|
+
/* 포커스 멈춤은 **보이는 포커스(:focus-visible)** 일 때만 — 마우스로 누른 단추(점·배너 단추)에 남은 포커스로 멈추면
|
|
85
|
+
마우스가 떠난 뒤에도 영영 멈춰 있었다. 키보드로 들어온 포커스는 그대로 멈춘다(0545ddf) */
|
|
86
|
+
onFocus: (e) => {
|
|
87
|
+
let 보임 = true;
|
|
88
|
+
try {
|
|
89
|
+
보임 = e.target.matches(':focus-visible');
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
보임 = true;
|
|
93
|
+
}
|
|
94
|
+
setFocused(보임);
|
|
95
|
+
}, onBlur: (e) => { if (!e.currentTarget.contains(e.relatedTarget))
|
|
38
96
|
setFocused(false); }, children: [_jsx("div", { className: "top-banner__viewport", children: _jsx("div", { className: "top-banner__track", style: { transform: `translateX(-${current * 100}%)` }, children: slides.map((slide, i) => (_jsx("div", { className: "top-banner__slide", "aria-hidden": i !== current, ...(i !== current ? { inert: true } : {}), children: slide }, i))) }) }), count > 1 && (_jsx("div", { className: `top-banner__dots${(dotTone?.[current] ?? 'light') === 'dark' ? ' top-banner__dots--dark' : ''}`, children: slides.map((_, i) => (_jsx("button", { type: "button", className: `top-banner__dot${i === current ? ' top-banner__dot--active' : ''}`, "aria-label": t('topBanner.dotAriaLabel', {
|
|
39
97
|
count, index: i + 1, defaultValue: `${count}개 중 ${i + 1}번째 배너`,
|
|
40
98
|
}), "aria-current": i === current, onClick: () => setIndex(i) }, i))) }))] }));
|
|
@@ -6,6 +6,7 @@ export default function WaveForm(props: {
|
|
|
6
6
|
segments?: Segment[] | null;
|
|
7
7
|
progress?: number;
|
|
8
8
|
totalDuration?: number;
|
|
9
|
+
/** @deprecated 읽지 않는다 — 구간 이동은 재생기의 번호 단추가 맡는다. 넘겨도 아무 일도 없다(지우면 넘기던 쪽 타입이 깨져 남겨 둔다). */
|
|
9
10
|
onSegmentClick?: (idx: number) => void;
|
|
10
11
|
waveProgressRef?: MutableRefObject<HTMLDivElement | null>;
|
|
11
12
|
noiseSrc?: string | null;
|
|
@@ -35,11 +35,32 @@ export default function WaveForm(props) {
|
|
|
35
35
|
// 이전/다음 연타로 트랙이 연속 변경되면 fetch+decode가 병렬로 떠서, 늦게 도착한
|
|
36
36
|
// 이전 트랙의 피크가 최신 파형을 덮어쓸 수 있다 — 세대 토큰으로 마지막 요청만 반영한다.
|
|
37
37
|
const peaksGenRef = useRef(0);
|
|
38
|
-
|
|
38
|
+
/** 칸의 지금 폭으로 막대 수·간격을 잰다. 칸이 없거나 폭이 0(숨은 칸)이면 폭 100 으로 본다(전과 같다). */
|
|
39
|
+
const measureBars = useCallback(() => {
|
|
40
|
+
const el = waveformRef.current;
|
|
41
|
+
const parentEl = outerRef && outerRef.current ? outerRef.current : (el ? el.closest('.audio-player__wave') : null);
|
|
42
|
+
const rect = parentEl ? parentEl.getBoundingClientRect() : (el ? el.getBoundingClientRect() : null);
|
|
43
|
+
const rawWidth = rect ? rect.width : 0;
|
|
44
|
+
const width = rect ? Math.max(100, rect.width) : 320;
|
|
45
|
+
const height = rect ? rect.height : 0;
|
|
46
|
+
const bars = Math.max(MIN_BARS, Math.min(MAX_BARS, Math.floor((width + BAR_GAP) / (BAR_WIDTH + BAR_GAP))));
|
|
47
|
+
const usedBarWidth = bars * BAR_WIDTH;
|
|
48
|
+
const gapCount = Math.max(0, bars - 1);
|
|
49
|
+
const gap = Math.max(0, gapCount > 0 ? (width - usedBarWidth) / gapCount : BAR_GAP);
|
|
50
|
+
return { bars, gap, height, width: rawWidth };
|
|
51
|
+
}, []);
|
|
52
|
+
/* 지금 그리는 주소와 막대 수 — 폭이 바뀌어 다시 잴 때 쓴다 */
|
|
53
|
+
const urlRef = useRef(undefined);
|
|
54
|
+
const barsRef = useRef(null);
|
|
55
|
+
const computePeaks = useCallback(async (url, barsParam, { quiet = false } = {}) => {
|
|
39
56
|
const gen = ++peaksGenRef.current;
|
|
40
57
|
const fresh = () => peaksGenRef.current === gen;
|
|
41
58
|
try {
|
|
42
|
-
|
|
59
|
+
/* quiet — 폭이 바뀌어 막대 수만 다시 뽑는 것. 이미 그려진 파형을 '준비 안 됨'으로 돌리지 않는다
|
|
60
|
+
(재생 중 파형이 꺼졌다 켜지거나 재생이 파형을 다시 기다리지 않게). */
|
|
61
|
+
if (!quiet)
|
|
62
|
+
setWaveReady(false);
|
|
63
|
+
urlRef.current = url;
|
|
43
64
|
if (!url) {
|
|
44
65
|
if (fresh())
|
|
45
66
|
setWavePeaks(null);
|
|
@@ -54,23 +75,17 @@ export default function WaveForm(props) {
|
|
|
54
75
|
await new Promise((r) => requestAnimationFrame(r));
|
|
55
76
|
}
|
|
56
77
|
await new Promise((r) => requestAnimationFrame(r));
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (height > 0)
|
|
63
|
-
setWaveHeightPx(height);
|
|
64
|
-
bars = Math.max(MIN_BARS, Math.min(MAX_BARS, Math.floor((width + BAR_GAP) / (BAR_WIDTH + BAR_GAP))));
|
|
65
|
-
const usedBarWidth = bars * BAR_WIDTH;
|
|
66
|
-
const gapCount = Math.max(0, bars - 1);
|
|
67
|
-
const computedGap = gapCount > 0 ? (width - usedBarWidth) / gapCount : BAR_GAP;
|
|
68
|
-
setBarGap(Math.max(0, computedGap));
|
|
78
|
+
const m = measureBars();
|
|
79
|
+
if (m.height > 0)
|
|
80
|
+
setWaveHeightPx(m.height);
|
|
81
|
+
bars = m.bars;
|
|
82
|
+
setBarGap(m.gap);
|
|
69
83
|
}
|
|
70
84
|
catch (e) {
|
|
71
85
|
bars = 120;
|
|
72
86
|
}
|
|
73
87
|
}
|
|
88
|
+
barsRef.current = bars;
|
|
74
89
|
const peaks = await computePeaksFromUrl(url, bars);
|
|
75
90
|
if (!fresh())
|
|
76
91
|
return;
|
|
@@ -94,7 +109,42 @@ export default function WaveForm(props) {
|
|
|
94
109
|
setWavePeaks(null);
|
|
95
110
|
setWaveReady(false);
|
|
96
111
|
}
|
|
97
|
-
}, [setWaveReady, setWavePeaks, setWaveHeightPx, setBarGap, setBarsTotal]);
|
|
112
|
+
}, [setWaveReady, setWavePeaks, setWaveHeightPx, setBarGap, setBarsTotal, measureBars]);
|
|
113
|
+
/* 막대 수는 칸 폭으로 정한다 — 전에는 처음 한 번만 재서, 숨은 칸(접힌 판·display:none 탭)에서 마운트되면
|
|
114
|
+
폭 0 → 17개로 굳어 칸이 드러난 뒤에도 성긴 파형이 남았다. 폭이 바뀌어 막대 수가 달라지면 다시 잰다.
|
|
115
|
+
ResizeObserver 가 없는 환경은 전처럼 처음 한 번만 잰다. */
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const el = outerRef.current;
|
|
118
|
+
if (!el || typeof ResizeObserver === 'undefined')
|
|
119
|
+
return;
|
|
120
|
+
let raf = null;
|
|
121
|
+
const ro = new ResizeObserver(() => {
|
|
122
|
+
if (raf !== null)
|
|
123
|
+
cancelAnimationFrame(raf);
|
|
124
|
+
raf = requestAnimationFrame(() => {
|
|
125
|
+
raf = null;
|
|
126
|
+
try {
|
|
127
|
+
const m = measureBars();
|
|
128
|
+
if (m.width <= 0)
|
|
129
|
+
return; // 아직(또 다시) 숨어 있다 — 잴 수 없다
|
|
130
|
+
if (m.height > 0)
|
|
131
|
+
setWaveHeightPx(m.height);
|
|
132
|
+
if (barsRef.current === null || m.bars === barsRef.current)
|
|
133
|
+
return;
|
|
134
|
+
const url = urlRef.current;
|
|
135
|
+
setBarGap(m.gap);
|
|
136
|
+
if (url)
|
|
137
|
+
computePeaks(url, m.bars, { quiet: true }).catch(() => { });
|
|
138
|
+
else
|
|
139
|
+
barsRef.current = m.bars;
|
|
140
|
+
}
|
|
141
|
+
catch (e) { }
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
ro.observe(el);
|
|
145
|
+
return () => { ro.disconnect(); if (raf !== null)
|
|
146
|
+
cancelAnimationFrame(raf); };
|
|
147
|
+
}, [measureBars, computePeaks]);
|
|
98
148
|
// src/segments 변경 시 파형을 다시 계산하는 의도된 fetch-kickoff이다
|
|
99
149
|
/* eslint-disable react-hooks/set-state-in-effect */
|
|
100
150
|
useEffect(() => {
|
|
@@ -12,8 +12,9 @@ export declare const NOISE_RATIO_MAX = 2;
|
|
|
12
12
|
export declare const NOISE_RATIO_STEP = 0.1;
|
|
13
13
|
/**
|
|
14
14
|
* @public 설정의 소음 비율(배). `noiseRatio` 가 있으면 그것(0~2 로 자름), 없으면 **옛 값에서
|
|
15
|
-
* 들리던 그대로** 옮긴다 — (level/9) ÷ mainVolume 을 0~2 로
|
|
16
|
-
*
|
|
15
|
+
* 들리던 그대로** 옮긴다 — (level/9) ÷ mainVolume 을 **반올림 없이** 0.1~2 로 조인다(말귀 `noiseRatioOfPct` 와 같은 범위).
|
|
16
|
+
* 전에는 0.1 칸으로 반올림해 옛 소리와 최대 0.05배 달라졌다(2026-09-15 결정). level 0(옛날에 소음이 안 들리던 기록)은
|
|
17
|
+
* 0 그대로 — 0.1 로 올리면 없던 소음이 생긴다. mainVolume 이 0 이면 옛 값으로는 비율을 알 수 없어 1.
|
|
17
18
|
*/
|
|
18
19
|
export declare function noiseRatioOf(settings: Pick<CoreNoiseSettings, 'level' | 'mainVolume'> & {
|
|
19
20
|
noiseRatio?: number;
|
|
@@ -27,11 +27,15 @@ export const NOISE_RATIO_MAX = 2;
|
|
|
27
27
|
/** @public 소음 비율 칸(배). */
|
|
28
28
|
export const NOISE_RATIO_STEP = 0.1;
|
|
29
29
|
const 비율자르기 = (r) => Math.min(NOISE_RATIO_MAX, Math.max(0, r));
|
|
30
|
-
|
|
30
|
+
/** 옛 기록에서 옮긴 비율의 범위 — 말귀 noiseRatioOfPct 와 같다(0.1~2). */
|
|
31
|
+
const 옛비율최소 = 0.1;
|
|
32
|
+
/* 부동소수 찌꺼기만 턴다(0.30000000000000004 → 0.3) — 0.1 칸으로 반올림하지는 않는다 */
|
|
33
|
+
const 찌꺼기털기 = (r) => Math.round(r * 1e6) / 1e6;
|
|
31
34
|
/**
|
|
32
35
|
* @public 설정의 소음 비율(배). `noiseRatio` 가 있으면 그것(0~2 로 자름), 없으면 **옛 값에서
|
|
33
|
-
* 들리던 그대로** 옮긴다 — (level/9) ÷ mainVolume 을 0~2 로
|
|
34
|
-
*
|
|
36
|
+
* 들리던 그대로** 옮긴다 — (level/9) ÷ mainVolume 을 **반올림 없이** 0.1~2 로 조인다(말귀 `noiseRatioOfPct` 와 같은 범위).
|
|
37
|
+
* 전에는 0.1 칸으로 반올림해 옛 소리와 최대 0.05배 달라졌다(2026-09-15 결정). level 0(옛날에 소음이 안 들리던 기록)은
|
|
38
|
+
* 0 그대로 — 0.1 로 올리면 없던 소음이 생긴다. mainVolume 이 0 이면 옛 값으로는 비율을 알 수 없어 1.
|
|
35
39
|
*/
|
|
36
40
|
export function noiseRatioOf(settings) {
|
|
37
41
|
const { noiseRatio } = settings;
|
|
@@ -41,7 +45,9 @@ export function noiseRatioOf(settings) {
|
|
|
41
45
|
const 전체 = Number(settings.mainVolume);
|
|
42
46
|
if (!(전체 > 0))
|
|
43
47
|
return 1;
|
|
44
|
-
|
|
48
|
+
if (!(소음 > 0))
|
|
49
|
+
return 0;
|
|
50
|
+
return 찌꺼기털기(Math.min(NOISE_RATIO_MAX, Math.max(옛비율최소, 소음 / 전체)));
|
|
45
51
|
}
|
|
46
52
|
const 전체소리 = (settings) => Math.min(1, Math.max(0, Number(settings.mainVolume) || 0));
|
|
47
53
|
/** @public 문제 소리 볼륨(0~1) = mainVolume. 비율과 상관없다. */
|
|
@@ -392,3 +392,10 @@
|
|
|
392
392
|
display: none;
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
/* 제거 배지 — 가리킬 수 있는 기기는 가리킬 때만, 터치 기기는 늘 보인다(색은 그대로). 늘 보이는 동안 같은 자리의 체크 배지는 숨긴다. */
|
|
396
|
+
@media (hover: hover) {
|
|
397
|
+
.training-block-button__remove:not(.is-hover) { display: none; }
|
|
398
|
+
}
|
|
399
|
+
@media (hover: none) {
|
|
400
|
+
.training-block-button__check--removable { display: none; }
|
|
401
|
+
}
|
|
@@ -184,7 +184,9 @@ export default function TrainingBlockButton(props) {
|
|
|
184
184
|
// 타입 라벨이 실제로 렌더되는지(타이틀과 같으면 숨김) — 레이아웃 여백 정리에 사용.
|
|
185
185
|
const typeLabelRendered = (!!showTypeLabel && !!type?.label && type.label !== String(itemTitle)) ||
|
|
186
186
|
(isSmall && !!trainingTypeName && trainingTypeName !== String(itemTitle));
|
|
187
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: btnRef, className: `training-block-button${isList ? ' training-block-button--list' : ''}${isRow ? ' training-block-button--row' : ''}${isCompact ? ' training-block-button--compact' : ''}${isSmall ? ' training-block-button--small' : ''}${isCompleted ? ' training-block-button--completed' : ''}${isSelected ? ' training-block-button--selected' : ''}${typeLabelRendered && !isSmall ? ' training-block-button--withType' : ''}${isSmall && !typeLabelRendered ? ' training-block-button--no-type-label' : ''}`, onClick: handleActivate, onPointerEnter: handleBtnHoverOn, onPointerLeave: handleBtnHoverClose, role: "button", tabIndex: 0, onKeyDown: handleKeyDown, "aria-label": typeof ariaLabel === 'string' ? ariaLabel : (typeof itemTitle === 'string' ? itemTitle : undefined),
|
|
187
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: btnRef, className: `training-block-button${isList ? ' training-block-button--list' : ''}${isRow ? ' training-block-button--row' : ''}${isCompact ? ' training-block-button--compact' : ''}${isSmall ? ' training-block-button--small' : ''}${isCompleted ? ' training-block-button--completed' : ''}${isSelected ? ' training-block-button--selected' : ''}${typeLabelRendered && !isSmall ? ' training-block-button--withType' : ''}${isSmall && !typeLabelRendered ? ' training-block-button--no-type-label' : ''}`, onClick: handleActivate, onPointerEnter: handleBtnHoverOn, onPointerLeave: handleBtnHoverClose, role: "button", tabIndex: 0, onKeyDown: handleKeyDown, "aria-label": typeof ariaLabel === 'string' ? ariaLabel : (typeof itemTitle === 'string' ? itemTitle : undefined),
|
|
188
|
+
/* tooltipText 만 주고 useDescriptionTooltip 을 안 켜면 글이 어디에도 안 떴다 — 그때는 title(가리키면 뜨고 낭독기엔 설명)로 준다 */
|
|
189
|
+
title: !useDescriptionTooltip && tooltipText ? (Array.isArray(tooltipText) ? tooltipText.filter(Boolean).join('\n') : tooltipText) || undefined : undefined, children: [_jsxs("div", { className: "training-block-button__left", children: [showTypeLabel && type?.label && type.label !== String(itemTitle) && (_jsx("span", { className: "training-block-button__type-label caption-1", children: type.label })), isSmall && trainingTypeName && trainingTypeName !== String(itemTitle) && (_jsx("span", { className: "training-block-button__type-label caption-2", children: trainingTypeName })), _jsx(CategoryIcon, { icon: iconForBtn, label: itemTitle, tooltip: popupIconTooltip, wrapperStyle: wrapperStyle, iconSize: (isSmall || isCompact) ? 'small' : 'medium' }), !hideCheckBadge && (isCompleted || isSelected) && !(onRemove && btnHover) && (_jsx(Badge, { className: onRemove ? 'training-block-button__check--removable' : undefined, type: "dot", icon: "check", position: "top-left", size: "medium", "aria-label": isCompleted ? t('training.completed', '훈련 완료') : t('training.selected', '선택됨') })), onRemove && (_jsx(Badge, { className: `training-block-button__remove${btnHover ? ' is-hover' : ''}`, type: "dot", icon: "remove", position: "top-left", size: "medium", "aria-label": t('common.remove', '제거'), onClick: handleRemoveClick })), _jsx("div", { className: `training-block-button__title ${isCompact ? 'body-3' : isSmall ? 'body-3b' : 'h3'}`, children: String(itemTitle) }), _jsxs("div", { className: "training-block-button__description", children: [(() => {
|
|
188
190
|
const countText = countLabelOverride !== undefined ? countLabelOverride : countLabel;
|
|
189
191
|
// 정해진 문항수가 없으면(빈 값) 카운트 요소 자체를 그리지 않는다.
|
|
190
192
|
return countText ? (_jsx("div", { className: `training-block-button__count ${isSmall ? 'caption-2' : 'caption-1'}`, children: countText })) : null;
|
|
@@ -2,6 +2,18 @@ import { type ReactNode, type FC, type CSSProperties, type RefObject, type Dispa
|
|
|
2
2
|
import './TrainingPagePopup.css';
|
|
3
3
|
import { type CoreNoiseSettings } from './NoisePlayerContext';
|
|
4
4
|
import { Page, ListenStep, ConfirmStep, ConfirmWithTextStep, OrderListStep, CardStep, CardOrderStep } from './TrainingStepComponents';
|
|
5
|
+
/** @public TrainingPagePopup `onComplete` 인자 */
|
|
6
|
+
export type TrainingCompleteResult = {
|
|
7
|
+
correct: number | null;
|
|
8
|
+
total: number | null;
|
|
9
|
+
/** 이 창에서 몇 번째 판인지(1부터) */
|
|
10
|
+
attempt?: number;
|
|
11
|
+
/** 다시하기 전 판들의 결과(앞 판부터) */
|
|
12
|
+
previousAttempts?: Array<{
|
|
13
|
+
correct: number | null;
|
|
14
|
+
total: number | null;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
5
17
|
/** @public */
|
|
6
18
|
export type { ScenarioPage, ScenarioStep, FlowState } from './trainingScenario';
|
|
7
19
|
import type { ScenarioPage, FlowState } from './trainingScenario';
|
|
@@ -39,11 +51,9 @@ export type TrainingPagePopupProps = {
|
|
|
39
51
|
stepIndex?: number;
|
|
40
52
|
stepCount?: number;
|
|
41
53
|
onBack?: () => void;
|
|
42
|
-
/** 완료 시 호출. 결과 팝업이 계산한 정답/전체 문항 수를 함께 전달(호출부가 무시해도 무방하도록 옵셔널 인자).
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
total: number | null;
|
|
46
|
-
}) => void;
|
|
54
|
+
/** 완료 시 호출. 결과 팝업이 계산한 정답/전체 문항 수를 함께 전달(호출부가 무시해도 무방하도록 옵셔널 인자).
|
|
55
|
+
* `attempt` 는 이 창에서 몇 번째 판인지(1부터, 다시하기마다 +1), `previousAttempts` 는 다시하기 전 판들의 정답/전체 — 전에는 1회차 결과가 어디에도 남지 않았다. */
|
|
56
|
+
onComplete?: (result?: TrainingCompleteResult) => void;
|
|
47
57
|
onNext?: () => void;
|
|
48
58
|
onSettings?: () => void;
|
|
49
59
|
/** 좌측 하단 훈련 설정 버튼 + 오디오 미터 토글 표시 여부. 기본 true — 모든 훈련에서 노출되며,
|
|
@@ -55,6 +65,7 @@ export type TrainingPagePopupProps = {
|
|
|
55
65
|
panelRef: RefObject<PanelElement | null>;
|
|
56
66
|
setResultOverlay: Dispatch<SetStateAction<unknown>>;
|
|
57
67
|
}) => ReactNode);
|
|
68
|
+
/** 대화상자 이름(낭독기가 읽는 이름). 주면 title·label·시나리오 제목보다 이긴다 — 보이는 제목과 다른 이름이 필요할 때. 없으면 보이는 제목. */
|
|
58
69
|
ariaLabel?: string;
|
|
59
70
|
label?: string;
|
|
60
71
|
buildResults?: (answers: Record<string, unknown>, meta?: Record<string, unknown>) => unknown;
|
|
@@ -85,6 +96,8 @@ export type TrainingPagePopupProps = {
|
|
|
85
96
|
contextsLength?: number | null;
|
|
86
97
|
onReplay?: () => void;
|
|
87
98
|
onClose?: () => void;
|
|
99
|
+
/** 결과를 아직 계산하는 중(비동기 buildResults 등) — true 인 동안 '완료' 를 막아야 한다. 기본 결과 창(TrainingResultPopup)은 그대로 받아 막는다. */
|
|
100
|
+
pending?: boolean;
|
|
88
101
|
}) => ReactNode;
|
|
89
102
|
};
|
|
90
103
|
/** @public */
|