@ihabdevteam/core 0.132.0 → 0.133.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 +65 -0
- package/dist/components.cjs.js +178 -178
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +182 -182
- package/dist/ihabdevteam-core.d.ts +1 -12
- package/dist/packages/core/src/components/BarChart.js +2 -2
- package/dist/packages/core/src/components/BottomNav.js +3 -3
- package/dist/packages/core/src/components/BulkBar.js +1 -1
- package/dist/packages/core/src/components/ChoiceChips.js +1 -1
- package/dist/packages/core/src/components/CodeBox.js +1 -1
- package/dist/packages/core/src/components/DatePicker.js +16 -3
- package/dist/packages/core/src/components/Dropdown.js +13 -3
- package/dist/packages/core/src/components/FileUpload.js +8 -7
- package/dist/packages/core/src/components/FilterPanel.js +8 -5
- package/dist/packages/core/src/components/FilterSectionInfo.js +1 -1
- package/dist/packages/core/src/components/Header.js +1 -1
- package/dist/packages/core/src/components/HearingDeviceInfo.js +1 -1
- package/dist/packages/core/src/components/PageTitle.js +1 -1
- package/dist/packages/core/src/components/Pagination.js +1 -1
- package/dist/packages/core/src/components/PanelRow.js +3 -3
- package/dist/packages/core/src/components/PaymentPlan.js +1 -1
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +2 -2
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +2 -2
- package/dist/packages/core/src/components/Popup/Popup.js +15 -3
- package/dist/packages/core/src/components/Popup/SaveTablePopup.js +1 -1
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +1 -1
- package/dist/packages/core/src/components/Popup/WordDataUpdatePopup.js +3 -3
- package/dist/packages/core/src/components/SegmentedControl.js +1 -1
- package/dist/packages/core/src/components/SideNav.js +1 -1
- package/dist/packages/core/src/components/Table.js +2 -2
- package/dist/packages/core/src/components/Tabs.js +7 -2
- package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +1 -1
- package/dist/packages/core/src/components/Tooltip/TextBoxTooltip.js +1 -1
- package/dist/packages/core/src/components/Tooltip/Tooltip.js +13 -5
- package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingBlock.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingBlockButton.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingCardGroup.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +51 -23
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingPagePopupTitle.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +26 -5
- package/dist/packages/core/src/components/Training/TrainingStepConfirm.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingStepTitle.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingWordCard.js +11 -6
- package/dist/packages/core/src/components/Training/useAudioPreview.js +7 -3
- package/dist/packages/core/src/components/UpdateAlert.js +1 -1
- package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +2 -2
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +3 -3
- package/dist/packages/core/src/components/Word/WordTableBlock.js +2 -2
- package/dist/packages/core/src/components/Word/WordTableButton.js +1 -1
- package/dist/packages/core/src/components/WordTable.js +6 -6
- package/dist/packages/core/src/components/useToastLifecycle.js +8 -1
- package/dist/packages/core/src/hooks/useAnchoredMenu.js +10 -9
- package/dist/packages/core/src/hooks/useDocumentTitle.d.ts +0 -11
- package/dist/packages/core/src/hooks/useDocumentTitle.js +16 -3
- package/dist/packages/core/src/hooks/useFocusTrap.js +6 -2
- package/dist/packages/core/src/utils/audioPeaks.js +13 -5
- package/dist/packages/core/src/utils/csv.js +3 -1
- package/dist/packages/core/src/utils/koreanSyllable.js +2 -1
- package/dist/packages/core/src/utils/textSort.d.ts +0 -10
- package/dist/packages/core/src/utils/textSort.js +7 -2
- package/dist/packages/core/src/utils/wordTrainingRuntime.js +28 -4
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +1 -1
- package/package.json +1 -1
|
@@ -292,12 +292,12 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
292
292
|
return (_jsx("div", { className: `table-wrap table-wrap--${size}${폭조절 ? ' table-wrap--resizable' : ''}${dragOver ? ' table-wrap--dragover' : ''}${collapsed ? ' table-wrap--collapsed' : ''}`, style: gridStyle, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop, children: _jsxs("div", { className: "table body-2", children: [hasHeader && (_jsxs("div", { className: "table-header", onClick: onToggleCollapse, role: onToggleCollapse ? 'button' : undefined, tabIndex: onToggleCollapse ? 0 : undefined, "aria-expanded": onToggleCollapse ? !collapsed : undefined, onKeyDown: onToggleCollapse ? (e) => { if (e.key === 'Enter' || e.key === ' ') {
|
|
293
293
|
e.preventDefault();
|
|
294
294
|
onToggleCollapse();
|
|
295
|
-
} } : undefined, children: [onToggleCollapse && (_jsx(Button, { size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-right' : 'chevron-down', ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), className: "table-header__chevron" })), icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "table-header__icon" })), _jsx("span", { className: "table-header__title h5b", children: title }), count !== undefined && _jsx("span", { className: "table-header__count body-3b", children: count }), headerActions && (_jsx("span", { className: "table-header__actions", onClick: e => e.stopPropagation(), children: headerActions }))] })), !collapsed && (_jsxs(_Fragment, { children: [headerGroups && headerGroups.length > 0 && (_jsx("div", { className: "table-fields table-fields--groups", children: headerGroups.map((g, i) => (_jsx("div", { className: cx('table-field body-3', g.className), style: { gridColumn: `span ${g.span}` }, children: g.label }, i))) })), _jsxs("div", { className: cx('table-fields-strip', 폭조절 && 'table-fields-strip--resizable'), children: [_jsx("div", { className: "table-fields", ref: fieldsRef, children: fields.map((f, i) => (_jsx(TableField, { onClick: f.onClick, className: f.className, sortDir: f.sortDir,
|
|
295
|
+
} } : undefined, children: [onToggleCollapse && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-right' : 'chevron-down', ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), className: "table-header__chevron" })), icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "table-header__icon" })), _jsx("span", { className: "table-header__title h5b", children: title }), count !== undefined && _jsx("span", { className: "table-header__count body-3b", children: count }), headerActions && (_jsx("span", { className: "table-header__actions", onClick: e => e.stopPropagation(), children: headerActions }))] })), !collapsed && (_jsxs(_Fragment, { children: [headerGroups && headerGroups.length > 0 && (_jsx("div", { className: "table-fields table-fields--groups", children: headerGroups.map((g, i) => (_jsx("div", { className: cx('table-field body-3', g.className), style: { gridColumn: `span ${g.span}` }, children: g.label }, i))) })), _jsxs("div", { className: cx('table-fields-strip', 폭조절 && 'table-fields-strip--resizable'), children: [_jsx("div", { className: "table-fields", ref: fieldsRef, children: fields.map((f, i) => (_jsx(TableField, { onClick: f.onClick, className: f.className, sortDir: f.sortDir,
|
|
296
296
|
/* fit 모드는 오른쪽 이웃에서 폭을 덜어 오므로, 덜어 올 컬럼이 없는 자리에는
|
|
297
297
|
손잡이를 두지 않는다 — 두면 그 컬럼만 grow처럼 넓어져 표가 밀린다. */
|
|
298
298
|
onResizeStart: 폭조절 && i < fields.length - 1 && f.resizable !== false
|
|
299
299
|
&& (resizeMode !== 'fit' || fields.some((g, j) => j > i && g.resizable !== false))
|
|
300
|
-
? (e) => startResize(i, e) : undefined, children: f.label }, i))) }), 폭조절 && (_jsx(Button, { size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: "chevrons-left-right-ellipsis", ariaLabel: t('table.resetColumns', '칸 너비 초기화'), showTooltipInfo: t('table.resetColumns', '칸 너비 초기화'), className: "table-fields__reset", onClick: 자동맞춤 }))] }), _jsx("div", { className: "table-body", children: loading ? (_jsx(TableRow, { variant: "loading" })) : items.length === 0 && empty ? (_jsx(TableRow, { variant: "empty", children: empty })) : items.map((item, idx) => {
|
|
300
|
+
? (e) => startResize(i, e) : undefined, children: f.label }, i))) }), 폭조절 && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: "chevrons-left-right-ellipsis", ariaLabel: t('table.resetColumns', '칸 너비 초기화'), showTooltipInfo: t('table.resetColumns', '칸 너비 초기화'), className: "table-fields__reset", onClick: 자동맞춤 }))] }), _jsx("div", { className: "table-body", children: loading ? (_jsx(TableRow, { variant: "loading" })) : items.length === 0 && empty ? (_jsx(TableRow, { variant: "empty", children: empty })) : items.map((item, idx) => {
|
|
301
301
|
const isDragging = draggingId != null && item.id === draggingId;
|
|
302
302
|
const 눌림 = !!onRowClick && (isRowClickable ? isRowClickable(item) : true);
|
|
303
303
|
const isExpanded = expandedId != null && (expandedId instanceof Set ? expandedId.has(item.id) : item.id === expandedId);
|
|
@@ -24,7 +24,12 @@ export default function Tabs({ items = [], value, onChange, className = '', size
|
|
|
24
24
|
const uid = useId();
|
|
25
25
|
const { t } = useTranslation();
|
|
26
26
|
const listRef = useRef(null);
|
|
27
|
-
|
|
27
|
+
/* value 가 없거나 items 에 없으면 **켜진 첫 탭**을 고른 것으로 본다. 전에는 무조건 첫 탭이라, 첫 탭이 꺼져 있으면
|
|
28
|
+
꺼진 탭의 판이 보이고 Tab 자리(tabIndex 0)가 꺼진 탭 하나뿐이어서 키보드로 탭 목록에 닿을 수 없었다. */
|
|
29
|
+
const 켜진첫키 = items.find(item => !item.disabled)?.key;
|
|
30
|
+
const activeKey = items.some(item => item.key === value) ? value : (켜진첫키 ?? items[0]?.key);
|
|
31
|
+
/* Tab 자리 — 고른 탭이 꺼져 있으면(부모가 꺼진 탭을 value 로 줌) 켜진 첫 탭이 받는다 */
|
|
32
|
+
const 탭자리키 = items.find(item => item.key === activeKey && !item.disabled)?.key ?? 켜진첫키;
|
|
28
33
|
/** 지금 화면에 선 **켜진 탭들** — 옮기는 자리는 전부 이 목록 기준이다 */
|
|
29
34
|
const 켜진탭들 = () => Array.from(listRef.current?.querySelectorAll('[role="tab"]:not(:disabled)') ?? []);
|
|
30
35
|
const focusTabAt = (index, tabs = 켜진탭들()) => {
|
|
@@ -58,5 +63,5 @@ export default function Tabs({ items = [], value, onChange, className = '', size
|
|
|
58
63
|
focusTabAt(tabs.length - 1, tabs);
|
|
59
64
|
};
|
|
60
65
|
const activeItem = items.find(item => item.key === activeKey);
|
|
61
|
-
return (_jsxs("div", { className: `tabs ${className}`.trim(), children: [_jsx("div", { ref: listRef, className: `tabs__list tabs__list--${size}`, role: "tablist", "aria-label": t('tabs.ariaLabel', '탭'), children: items.map(item => (_jsx("button", { id: `tabs__tab-${uid}-${item.key}`, type: "button", role: "tab", "aria-selected": item.key === activeKey, "aria-controls": `tabs__panel-${uid}-${item.key}`, disabled: item.disabled, tabIndex: item.key ===
|
|
66
|
+
return (_jsxs("div", { className: `tabs ${className}`.trim(), children: [_jsx("div", { ref: listRef, className: `tabs__list tabs__list--${size}`, role: "tablist", "aria-label": t('tabs.ariaLabel', '탭'), children: items.map(item => (_jsx("button", { id: `tabs__tab-${uid}-${item.key}`, type: "button", role: "tab", "aria-selected": item.key === activeKey, "aria-controls": `tabs__panel-${uid}-${item.key}`, disabled: item.disabled, tabIndex: item.key === 탭자리키 ? 0 : -1, className: `tabs__tab body-2${item.key === activeKey ? ' tabs__tab--active' : ''}`, onClick: () => onChange?.(item.key), onKeyDown: handleKeyDown, children: item.label }, item.key))) }), activeItem && (_jsx("div", { id: `tabs__panel-${uid}-${activeItem.key}`, role: "tabpanel", "aria-labelledby": `tabs__tab-${uid}-${activeItem.key}`, tabIndex: 0, className: "tabs__panel", children: activeItem.panel }, activeItem.key))] }));
|
|
62
67
|
}
|
|
@@ -84,7 +84,7 @@ export default function MenuTooltip({ columns = [], onClose, className = '', siz
|
|
|
84
84
|
}
|
|
85
85
|
catch (err) { }
|
|
86
86
|
}, [onClose]);
|
|
87
|
-
return (_jsxs("div", { className: `tooltip-menu tooltip-menu--${size} ${className}`, children: [(stack.length > 1 || current.title) && (_jsxs("div", { className: "tooltip-menu__header", children: [stack.length > 1 ? (_jsx(Button, { size: itemSize, type: "dot", icon: "chevron-left", ariaLabel: t('common.back', '뒤로'), onClick: goBack, variant: "transparent-gray", useBorder: false })) : null, _jsx("div", { className: "tooltip-menu__title button-1", children: current.title })] })), _jsx("div", { className: "tooltip-menu__columns", ref: itemsWrapRef, children: current.columns && current.columns.map((col, ci) => (_jsxs("div", { className: "tooltip-menu__column", children: [col.title ? _jsx("div", { className: "tooltip-menu__column-title body-3", children: col.title }) : null, _jsx("div", { className: "tooltip-menu__items", children: (col.items || []).map((item, idx) => (_jsx(Button, { className: "tooltip-menu__item", size: itemSize, type: item.label ? 'bar' : 'dot', icon: item.selected ? 'check' : (item.icon || 'none'), rightIcon: Array.isArray(item.children) && item.children.length
|
|
87
|
+
return (_jsxs("div", { className: `tooltip-menu tooltip-menu--${size} ${className}`, children: [(stack.length > 1 || current.title) && (_jsxs("div", { className: "tooltip-menu__header", children: [stack.length > 1 ? (_jsx(Button, { htmlType: "button", size: itemSize, type: "dot", icon: "chevron-left", ariaLabel: t('common.back', '뒤로'), onClick: goBack, variant: "transparent-gray", useBorder: false })) : null, _jsx("div", { className: "tooltip-menu__title button-1", children: current.title })] })), _jsx("div", { className: "tooltip-menu__columns", ref: itemsWrapRef, children: current.columns && current.columns.map((col, ci) => (_jsxs("div", { className: "tooltip-menu__column", children: [col.title ? _jsx("div", { className: "tooltip-menu__column-title body-3", children: col.title }) : null, _jsx("div", { className: "tooltip-menu__items", children: (col.items || []).map((item, idx) => (_jsx(Button, { htmlType: "button", className: "tooltip-menu__item", size: itemSize, type: item.label ? 'bar' : 'dot', icon: item.selected ? 'check' : (item.icon || 'none'), rightIcon: Array.isArray(item.children) && item.children.length
|
|
88
88
|
? 'chevron-right'
|
|
89
89
|
: item.badgeCount
|
|
90
90
|
? _jsx(Badge, { type: "dot", position: "inline", number: Math.min(item.badgeCount, 99), size: 24, "aria-label": `${item.badgeCount}` })
|
|
@@ -47,7 +47,7 @@ export default function TextBoxTooltip(props) {
|
|
|
47
47
|
'--tt-line-height': `${lineHeight}px`,
|
|
48
48
|
'--tt-marker-size': '28px',
|
|
49
49
|
};
|
|
50
|
-
const inner = (_jsxs("div", { className: `tooltip-textbox__root ${theme === 'dark' ? 'is-dark' : ''}`, children: [_jsxs("div", { className: "tooltip-textbox__box", children: [label && _jsx("div", { className: "tooltip-textbox__label body-2b", children: label }), _jsx("div", { className: "tooltip-textbox__text body-1", style: textStyle, children: lines.map((ln, idx) => (_jsxs("div", { className: "tooltip-textbox__line", children: [lines.length > 1 ? (_jsx("div", { className: "tooltip-textbox__marker", children: _jsx("span", { className: "tooltip-textbox__line-number button-1", children: idx + 1 }) })) : null, _jsx("span", { className: "tooltip-textbox__line-text", children: ln })] }, idx))) })] }), _jsxs("div", { className: "tooltip-textbox__buttons", children: [_jsx(Button, { size: 32, showTooltipInfo: true, type: "dot", useBorder: false, width: 48, icon: 'minus', onClick: decrease, ariaLabel: t('training.fontSizeDecrease', '글자 크기 감소'), variant: theme === 'dark' ? 'transparent-white' : 'transparent-gray', disabled: !canDecrease }), _jsx(Button, { size: 32, showTooltipInfo: true, type: "dot", useBorder: false, width: 48, icon: 'plus', onClick: increase, ariaLabel: t('training.fontSizeIncrease', '글자 크기 증가'), variant: theme === 'dark' ? 'transparent-white' : 'transparent-gray', disabled: !canIncrease }), _jsx(Button, { size: 32, showTooltipInfo: true, type: "dot", useBorder: false, width: 48, icon: theme === 'dark' ? 'moon' : 'sun-medium', onClick: toggleTheme, ariaLabel: t('training.toggleBackground', '배경 전환'), variant: theme === 'dark' ? 'transparent-white' : 'transparent-gray' }), _jsx(Button, { size: 32, showTooltipInfo: true, type: "dot", useBorder: false, width: 48, icon: 'x', onClick: handleClose, ariaLabel: t('common.close', '닫기'), variant: theme === 'dark' ? 'transparent-white' : 'transparent-gray' })] })] }));
|
|
50
|
+
const inner = (_jsxs("div", { className: `tooltip-textbox__root ${theme === 'dark' ? 'is-dark' : ''}`, children: [_jsxs("div", { className: "tooltip-textbox__box", children: [label && _jsx("div", { className: "tooltip-textbox__label body-2b", children: label }), _jsx("div", { className: "tooltip-textbox__text body-1", style: textStyle, children: lines.map((ln, idx) => (_jsxs("div", { className: "tooltip-textbox__line", children: [lines.length > 1 ? (_jsx("div", { className: "tooltip-textbox__marker", children: _jsx("span", { className: "tooltip-textbox__line-number button-1", children: idx + 1 }) })) : null, _jsx("span", { className: "tooltip-textbox__line-text", children: ln })] }, idx))) })] }), _jsxs("div", { className: "tooltip-textbox__buttons", children: [_jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", useBorder: false, width: 48, icon: 'minus', onClick: decrease, ariaLabel: t('training.fontSizeDecrease', '글자 크기 감소'), variant: theme === 'dark' ? 'transparent-white' : 'transparent-gray', disabled: !canDecrease }), _jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", useBorder: false, width: 48, icon: 'plus', onClick: increase, ariaLabel: t('training.fontSizeIncrease', '글자 크기 증가'), variant: theme === 'dark' ? 'transparent-white' : 'transparent-gray', disabled: !canIncrease }), _jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", useBorder: false, width: 48, icon: theme === 'dark' ? 'moon' : 'sun-medium', onClick: toggleTheme, ariaLabel: t('training.toggleBackground', '배경 전환'), variant: theme === 'dark' ? 'transparent-white' : 'transparent-gray' }), _jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", useBorder: false, width: 48, icon: 'x', onClick: handleClose, ariaLabel: t('common.close', '닫기'), variant: theme === 'dark' ? 'transparent-white' : 'transparent-gray' })] })] }));
|
|
51
51
|
if (anchorRect) {
|
|
52
52
|
return (_jsx(Tooltip, { anchorRect: anchorRect, className: `tooltip-textbox ${theme === 'dark' ? 'is-dark' : ''}`, onClose: onClose, closeOnOutsideClick: closeOnOutsideClick, isMobilePopup: isMobilePopup,
|
|
53
53
|
/* 이름 없는 `role="dialog"` 는 "대화상자" 로만 읽힌다 — 보이는 라벨을 쓴다. */
|
|
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState, useLayoutEffect } from 'react
|
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import './Tooltip.css';
|
|
6
6
|
import { FOCUSABLE_SELECTOR } from '../../hooks/useFocusTrap';
|
|
7
|
+
import { pushEsc } from '../../hooks/escStack';
|
|
7
8
|
/** @public */
|
|
8
9
|
export default function Tooltip(props) {
|
|
9
10
|
const { anchorRect, anchorEl, offset = 20, edgeMargin = 20, onClose, closeOnOutsideClick = true, className = '', role = 'dialog', ariaLabel = '', children, backdrop = false, isMobilePopup = false, useBorder = true, header, } = props || {};
|
|
@@ -242,11 +243,6 @@ export default function Tooltip(props) {
|
|
|
242
243
|
}
|
|
243
244
|
};
|
|
244
245
|
const handleKeyDown = (e) => {
|
|
245
|
-
if (e.key === 'Escape') {
|
|
246
|
-
e.stopPropagation();
|
|
247
|
-
startClose();
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
246
|
if (e.key === 'Tab') {
|
|
251
247
|
const el = ref.current;
|
|
252
248
|
if (!el)
|
|
@@ -285,6 +281,18 @@ export default function Tooltip(props) {
|
|
|
285
281
|
document.removeEventListener('keydown', handleKeyDown);
|
|
286
282
|
};
|
|
287
283
|
}, [onClose, closeOnOutsideClick, startClose, isInline]);
|
|
284
|
+
/* ESC — **공유 ESC 스택**에 올린다. 전에는 document 에서 직접 들었는데, 스택(Popup)도 같은 document
|
|
285
|
+
리스너라 stopPropagation 이 서로를 못 막아 팝업 안 툴팁에서 Esc 한 번에 툴팁과 팝업이 함께 닫혔다.
|
|
286
|
+
startClose 는 closing 이 바뀔 때마다 새로 만들어지므로 ref 로 부른다 — 다시 올리면 스택 차례가
|
|
287
|
+
뒤섞인다. 닫을 곳(onClose)이 없는 툴팁은 올리지 않는다(올리면 Esc 를 삼켜 아래 팝업이 안 닫힌다). */
|
|
288
|
+
const startCloseRef = useRef(startClose);
|
|
289
|
+
useEffect(() => { startCloseRef.current = startClose; });
|
|
290
|
+
const hasOnClose = !!onClose;
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
if (isInline || !hasOnClose)
|
|
293
|
+
return;
|
|
294
|
+
return pushEsc(() => startCloseRef.current());
|
|
295
|
+
}, [isInline, hasOnClose]);
|
|
288
296
|
// Allow external callers to force-close all tooltips by dispatching
|
|
289
297
|
// a `speechear-close-tooltips` CustomEvent on `document`.
|
|
290
298
|
useEffect(() => {
|
|
@@ -74,7 +74,7 @@ export default function NoiseSettingsTooltip({ anchorRect, onClose }) {
|
|
|
74
74
|
}
|
|
75
75
|
catch (e) { }
|
|
76
76
|
}, [noisePlayer]);
|
|
77
|
-
return (_jsx(Tooltip, { anchorRect: anchorRect, offset: 12, onClose: onClose, ariaLabel: t('training.trainingSettingsTitle', '훈련 설정'), isMobilePopup: false, closeOnOutsideClick: false, header: _jsxs("div", { className: "noise-settings__header", children: [_jsx("span", { children: t('training.trainingSettingsTitle', '훈련 설정') }), _jsx(Button, { size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, ariaLabel: t('common.close', '닫기'), onClick: onClose })] }), children: _jsxs(Panel, { className: "noise-settings", useBorder: false, labelWidth: 72, children: [_jsx(SliderPanelRow, { title: t('training.playVolumeTitle', '전체 소리'), valueLabel: `${volPct}%`, value: volPct, min: 0, max: 100, step: 10, size: sliderSize, onChange: handlePlayVolumeChange }), _jsx(SliderPanelRow, { title: t('training.playSpeedTitle', '재생 속도'), valueLabel: `${playbackRate.toFixed(2)}x`, value: playbackRate, min: PLAY_SPEED_MIN, max: PLAY_SPEED_MAX, step: PLAY_SPEED_STEP, size: sliderSize, onChange: handlePlaySpeedChange }), _jsx(PanelRow, { variant: "custom", label: t('training.noiseTypeTitle', '소음 종류'), useFullWidth: true, children: _jsx(SegmentedControl, { ariaLabel: t('training.noiseTypeTitle', '소음 종류'), size: 40, shape: "rounded", options: [
|
|
77
|
+
return (_jsx(Tooltip, { anchorRect: anchorRect, offset: 12, onClose: onClose, ariaLabel: t('training.trainingSettingsTitle', '훈련 설정'), isMobilePopup: false, closeOnOutsideClick: false, header: _jsxs("div", { className: "noise-settings__header", children: [_jsx("span", { children: t('training.trainingSettingsTitle', '훈련 설정') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, ariaLabel: t('common.close', '닫기'), onClick: onClose })] }), children: _jsxs(Panel, { className: "noise-settings", useBorder: false, labelWidth: 72, children: [_jsx(SliderPanelRow, { title: t('training.playVolumeTitle', '전체 소리'), valueLabel: `${volPct}%`, value: volPct, min: 0, max: 100, step: 10, size: sliderSize, onChange: handlePlayVolumeChange }), _jsx(SliderPanelRow, { title: t('training.playSpeedTitle', '재생 속도'), valueLabel: `${playbackRate.toFixed(2)}x`, value: playbackRate, min: PLAY_SPEED_MIN, max: PLAY_SPEED_MAX, step: PLAY_SPEED_STEP, size: sliderSize, onChange: handlePlaySpeedChange }), _jsx(PanelRow, { variant: "custom", label: t('training.noiseTypeTitle', '소음 종류'), useFullWidth: true, children: _jsx(SegmentedControl, { ariaLabel: t('training.noiseTypeTitle', '소음 종류'), size: 40, shape: "rounded", options: [
|
|
78
78
|
{ key: NONE_KEY, label: t('training.noise.type.none', '없음') },
|
|
79
79
|
...getNoiseTypeOptions(t).map(opt => ({ key: opt.value, label: opt.label })),
|
|
80
80
|
], value: noiseEnabled ? noiseType : NONE_KEY, onChange: handleTypeChange }) }), _jsx(SliderPanelRow, { title: t('training.noiseRatioTitle', '소음 비율'), valueLabel: t('training.noiseRatioValue', '{{ratio}}배', { ratio: ratio.toFixed(1) }), value: ratio, min: 0, max: NOISE_RATIO_MAX, step: NOISE_RATIO_STEP, size: sliderSize, onChange: handleRatioChange })] }) }));
|
|
@@ -40,5 +40,5 @@ export default function TrainingBlock(props) {
|
|
|
40
40
|
: undefined, onMore: typeof onMore === 'function'
|
|
41
41
|
? (e) => onMore(itemForBtn, e)
|
|
42
42
|
: undefined }, itemForBtn?.id != null ? `${type?.key || 't'}-${String(itemForBtn.id)}` : `${type?.key || 't'}-${idx}`));
|
|
43
|
-
}) }), total > BTN_COUNT_MAX && visible < total && (_jsx("div", { className: "training-block__more", children: _jsx(Button, { size: 48, type: "bar", icon: "plus", onClick: loadMore, ariaLabel: t('common.loadMore', '더보기'), label: t('common.loadMore', '더보기'), variant: "main" }) }))] }));
|
|
43
|
+
}) }), total > BTN_COUNT_MAX && visible < total && (_jsx("div", { className: "training-block__more", children: _jsx(Button, { htmlType: "button", size: 48, type: "bar", icon: "plus", onClick: loadMore, ariaLabel: t('common.loadMore', '더보기'), label: t('common.loadMore', '더보기'), variant: "main" }) }))] }));
|
|
44
44
|
}
|
|
@@ -178,7 +178,7 @@ export default function TrainingBlockButton(props) {
|
|
|
178
178
|
const countText = countLabelOverride !== undefined ? countLabelOverride : countLabel;
|
|
179
179
|
// 정해진 문항수가 없으면(빈 값) 카운트 요소 자체를 그리지 않는다.
|
|
180
180
|
return countText ? (_jsx("div", { className: `training-block-button__count ${isSmall ? 'caption-2' : 'caption-1'}`, children: countText })) : null;
|
|
181
|
-
})(), _jsx("div", { className: "training-block-button__chevron", children: _jsx(Icon, { icon: "chevron-right", label: t('training.next', '다음'), className: "training-block-button__chevron-icon", imgClassName: "training-block-button__chevron-icon-image", maskClassName: "training-block-button__chevron-icon-mask", svgClassName: "training-block-button__chevron-icon-svg", size: isSmall ? 12 : 20 }) })] }), (isList || isSmall) && previewUrls.length > 0 && (_jsx("div", { ref: previewBtnRef, className: "training-block-button__preview", "data-tooltip-ignore": true, onClick: handlePreviewAreaClick, onPointerEnter: handlePreviewHoverOn, onPointerLeave: handlePreviewHoverClose, children: _jsx(Button, { size: isSmall ? 28 : 32, type: "dot", icon: playing ? 'pause' : 'play', variant: isSmall ? 'transparent-lightGray' : 'default', useBorder: false, selected: playing, ariaLabel: playing ? t('training.stop', '정지') : t('training.preview', '미리 듣기'), onClick: handlePreview }) }))] }), onMore && (_jsx("div", { className: "training-block-button__more", "data-tooltip-ignore": true, onClick: e => { e.stopPropagation(); onMore(e); }, children: _jsx(Button, { size: 40, type: "dot", icon: "more-horizontal", variant: "transparent-lightGray", useBorder: false, ariaLabel: t('common.more', '더보기') }) }))] }), previewHover && (_jsx(InfoTooltip, { anchorRect: previewAnchorRect, text: playing ? t('training.stop', '정지') : t('training.preview', '미리 듣기'), offset: 8, onClose: handlePreviewHoverClose })), btnHover && !open && useDescriptionTooltip && (tooltipItems ? (_jsx(DescriptionTooltip, { anchorRect: btnAnchorRect, items: tooltipItems, activeIndex: playingIndex, onClose: handleBtnHoverClose })) : topic?.subTitle ? (_jsx(InfoTooltip, { anchorRect: btnAnchorRect, text: topic.subTitle, offset: 8, onClose: handleBtnHoverClose })) : null), open && renderPopup && renderPopup({
|
|
181
|
+
})(), _jsx("div", { className: "training-block-button__chevron", children: _jsx(Icon, { icon: "chevron-right", label: t('training.next', '다음'), className: "training-block-button__chevron-icon", imgClassName: "training-block-button__chevron-icon-image", maskClassName: "training-block-button__chevron-icon-mask", svgClassName: "training-block-button__chevron-icon-svg", size: isSmall ? 12 : 20 }) })] }), (isList || isSmall) && previewUrls.length > 0 && (_jsx("div", { ref: previewBtnRef, className: "training-block-button__preview", "data-tooltip-ignore": true, onClick: handlePreviewAreaClick, onPointerEnter: handlePreviewHoverOn, onPointerLeave: handlePreviewHoverClose, children: _jsx(Button, { htmlType: "button", size: isSmall ? 28 : 32, type: "dot", icon: playing ? 'pause' : 'play', variant: isSmall ? 'transparent-lightGray' : 'default', useBorder: false, selected: playing, ariaLabel: playing ? t('training.stop', '정지') : t('training.preview', '미리 듣기'), onClick: handlePreview }) }))] }), onMore && (_jsx("div", { className: "training-block-button__more", "data-tooltip-ignore": true, onClick: e => { e.stopPropagation(); onMore(e); }, children: _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "more-horizontal", variant: "transparent-lightGray", useBorder: false, ariaLabel: t('common.more', '더보기') }) }))] }), previewHover && (_jsx(InfoTooltip, { anchorRect: previewAnchorRect, text: playing ? t('training.stop', '정지') : t('training.preview', '미리 듣기'), offset: 8, onClose: handlePreviewHoverClose })), btnHover && !open && useDescriptionTooltip && (tooltipItems ? (_jsx(DescriptionTooltip, { anchorRect: btnAnchorRect, items: tooltipItems, activeIndex: playingIndex, onClose: handleBtnHoverClose })) : topic?.subTitle ? (_jsx(InfoTooltip, { anchorRect: btnAnchorRect, text: topic.subTitle, offset: 8, onClose: handleBtnHoverClose })) : null), open && renderPopup && renderPopup({
|
|
182
182
|
trainingData: topic,
|
|
183
183
|
typeKey: type?.key ?? '',
|
|
184
184
|
onClose: handleClose,
|
|
@@ -10,7 +10,7 @@ export default function TrainingCardGroup({ title, count, collapsed = false, onT
|
|
|
10
10
|
/* 접을 수 있는가 — `onToggle` 유무가 그것을 말한다. 새 prop 을 두지 않는다. */
|
|
11
11
|
const 접을수있다 = onToggle != null;
|
|
12
12
|
const cls = cx('training-card-group', dragOver && 'training-card-group--dragover', collapsed && 'training-card-group--collapsed', 접을수있다 && 'training-card-group--collapsible', className);
|
|
13
|
-
return (_jsxs("div", { className: cls, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop, children: [_jsxs("header", { className: "training-card-group__header", children: [접을수있다 && (_jsx(Button, { size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-right' : 'chevron-down', ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), className: "training-card-group__chevron", onClick: onToggle })), typeof title === 'string'
|
|
13
|
+
return (_jsxs("div", { className: cls, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop, children: [_jsxs("header", { className: "training-card-group__header", children: [접을수있다 && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-right' : 'chevron-down', ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), className: "training-card-group__chevron", onClick: onToggle })), typeof title === 'string'
|
|
14
14
|
? _jsx("span", { className: "training-card-group__title h5b", children: title })
|
|
15
15
|
: title, count !== undefined && _jsx("span", { className: "training-card-group__count body-3b", children: count }), (sort || actions != null) && (_jsxs("span", { className: "training-card-group__actions", onClick: e => e.stopPropagation(), children: [sort && (_jsx(Dropdown, { size: 40, options: sort.options, value: sort.value, placeholder: sort.placeholder, onChange: sort.onChange })), actions] }))] }), !collapsed && (empty
|
|
16
16
|
? _jsx("div", { className: "training-card-group__empty body-3", children: emptyContent })
|
|
@@ -5,6 +5,7 @@ import { PAGE_POPUP_CLOSE_MS } from './pagePopupTiming';
|
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { createPortal } from "react-dom";
|
|
7
7
|
import { useFocusTrap, FOCUSABLE_SELECTOR } from '../../hooks/useFocusTrap';
|
|
8
|
+
import { pushEsc } from '../../hooks/escStack';
|
|
8
9
|
import Toast from '../Toast';
|
|
9
10
|
import Button from '../Button';
|
|
10
11
|
import './TrainingPagePopup.css';
|
|
@@ -122,17 +123,29 @@ function TrainingPagePopupInner(props) {
|
|
|
122
123
|
// 이 판에만 거는 설정 — 마운트 때 한 번 붙잡는다(부모가 매 렌더 새 함수를 넘겨도 다시 걸지 않게).
|
|
123
124
|
// 있으면 이 판은 저장을 건너뛴다(mergeAndSaveSettings 참고).
|
|
124
125
|
const noiseOverrideRef = useRef(noiseOverride);
|
|
126
|
+
/* 불러오기가 끝났나 · 끝나기 전에 바뀐 것. 끝나기 전의 변경은 **저장하지 않고 모아 두었다가** 불러온
|
|
127
|
+
값 위에 얹어 한 번 저장한다. 전에는 enabled:false 초기값 위에 합쳐 곧바로 저장해(소음 꺼짐이 DB 에
|
|
128
|
+
남아 다음 판부터 소음이 안 났다) 늦게 온 불러오기가 그 변경을 다시 덮었다. */
|
|
129
|
+
const noiseLoadedRef = useRef(false);
|
|
130
|
+
const pendingNoiseRef = useRef(null);
|
|
125
131
|
// 계정/고객 저장소에서 초기값 로드 (앱이 configureNoiseSettingsStore로 주입)
|
|
126
132
|
useEffect(() => {
|
|
127
133
|
let cancelled = false;
|
|
128
|
-
|
|
134
|
+
const apply = (saved) => {
|
|
129
135
|
if (cancelled)
|
|
130
136
|
return;
|
|
137
|
+
noiseLoadedRef.current = true;
|
|
131
138
|
// 저장된 설정이 있으면 병합, 없으면 진짜 기본값으로 확정 — 어느 쪽이든 이제부터 재생 허용.
|
|
132
139
|
// mergeNoiseSettings — noiseRatio 가 없는 옛 기록은 기본값 비율(1)에 가리지 않고 level 에서 옮긴다.
|
|
133
|
-
|
|
140
|
+
// 바탕은 **처음 값**이다 — 끝나기 전 변경은 pending 으로 따로 얹는다(아래).
|
|
141
|
+
const loaded = saved ? mergeNoiseSettings({ ...DEFAULT_NOISE_SETTINGS, enabled: false }, saved) : { ...DEFAULT_NOISE_SETTINGS };
|
|
142
|
+
const pending = pendingNoiseRef.current;
|
|
143
|
+
pendingNoiseRef.current = null;
|
|
144
|
+
const merged = pending ? mergeNoiseSettings(loaded, pending) : loaded;
|
|
134
145
|
noiseSettingsRef.current = merged;
|
|
135
146
|
setNoisePart(merged);
|
|
147
|
+
if (pending && !noiseOverrideRef.current)
|
|
148
|
+
saveNoiseSettings(merged);
|
|
136
149
|
reportAppliedNoiseSettings(merged);
|
|
137
150
|
// override 는 병합이 끝난 **뒤에** 덮는다 — 로드가 늦게 와도 override 가 이긴다.
|
|
138
151
|
const override = noiseOverrideRef.current;
|
|
@@ -143,7 +156,8 @@ function TrainingPagePopupInner(props) {
|
|
|
143
156
|
setNoisePart(applied);
|
|
144
157
|
reportAppliedNoiseSettings(applied);
|
|
145
158
|
}
|
|
146
|
-
}
|
|
159
|
+
};
|
|
160
|
+
void loadNoiseSettings().then(apply);
|
|
147
161
|
return () => { cancelled = true; };
|
|
148
162
|
}, []);
|
|
149
163
|
// 오디오 레벨 미터용 analyser (오디오 플레이어가 재생 시 등록)
|
|
@@ -160,6 +174,12 @@ function TrainingPagePopupInner(props) {
|
|
|
160
174
|
const merged = mergeNoiseSettings(noiseSettingsRef.current, next);
|
|
161
175
|
noiseSettingsRef.current = merged;
|
|
162
176
|
setNoisePart(merged);
|
|
177
|
+
if (!noiseLoadedRef.current) {
|
|
178
|
+
// 불러오기 전 — 화면에는 곧바로 걸되 저장은 불러온 뒤로 미룬다(위 apply).
|
|
179
|
+
pendingNoiseRef.current = { ...(pendingNoiseRef.current ?? {}), ...next };
|
|
180
|
+
reportAppliedNoiseSettings(merged);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
163
183
|
// noiseOverride 판은 저장하지 않는다 — 판 중에 바꾼 것도 이 판에만.
|
|
164
184
|
if (!noiseOverrideRef.current)
|
|
165
185
|
saveNoiseSettings(merged);
|
|
@@ -199,31 +219,39 @@ function TrainingPagePopupInner(props) {
|
|
|
199
219
|
}, []);
|
|
200
220
|
useEffect(() => () => { if (closeTimerRef.current)
|
|
201
221
|
clearTimeout(closeTimerRef.current); }, []);
|
|
202
|
-
|
|
222
|
+
/* 열 때 첫 칸으로 포커스 · 닫힐 때 연 곳으로 되돌림 — **마운트 때 한 번만.** 가둠은 useFocusTrap 이 한다.
|
|
223
|
+
전에는 이 효과가 onBack 에 걸려 있어 부모가 새 onBack 을 넘길 때마다 다시 돌아 **훈련 중 포커스를 첫 칸으로
|
|
224
|
+
끌어왔고**, 닫을 때 포커스를 되돌리는 코드는 없어 뒤 화면에서 포커스를 잃었다. */
|
|
225
|
+
const onBackRef = useRef(onBack);
|
|
226
|
+
useEffect(() => { onBackRef.current = onBack; });
|
|
203
227
|
useEffect(() => {
|
|
204
228
|
const panel = panelRef.current;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
229
|
+
const previouslyFocused = document.activeElement;
|
|
230
|
+
if (panel) {
|
|
231
|
+
const firstEl = Array.from(panel.querySelectorAll(FOCUSABLE_SELECTOR)).filter(el => !el.closest('[aria-hidden="true"]'))[0];
|
|
232
|
+
if (firstEl) {
|
|
233
|
+
try {
|
|
234
|
+
firstEl.focus();
|
|
235
|
+
}
|
|
236
|
+
catch (e) { }
|
|
213
237
|
}
|
|
214
|
-
catch (e) { }
|
|
215
238
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
239
|
+
return () => {
|
|
240
|
+
// 포커스가 이 창 안이거나 갈 곳을 잃었을 때만 — 이미 다른 창으로 옮겨 갔으면 빼앗지 않는다(Popup 과 같다)
|
|
241
|
+
const active = document.activeElement;
|
|
242
|
+
if (!active || active === document.body || (panel && panel.contains(active))) {
|
|
243
|
+
try {
|
|
244
|
+
previouslyFocused?.focus?.();
|
|
245
|
+
}
|
|
246
|
+
catch (e) { }
|
|
222
247
|
}
|
|
223
248
|
};
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
249
|
+
}, []);
|
|
250
|
+
/* ESC — **공유 ESC 스택**에 올린다(Popup·메뉴·툴팁과 같은 스택). 전에는 판(panel)의 네이티브 keydown 만 들었는데
|
|
251
|
+
판에 tabIndex 가 없어, 문제 글·그림을 눌러 포커스가 body 로 가면 Esc 가 먹지 않았고, 훈련 창이 다른 팝업 위에
|
|
252
|
+
떠 있을 때는 그 팝업의 document 리스너가 가려진 아래 팝업을 닫았다. */
|
|
253
|
+
useEffect(() => pushEsc(() => scheduleClose(() => { const back = onBackRef.current; if (typeof back === 'function')
|
|
254
|
+
back(); })), [scheduleClose]);
|
|
227
255
|
/* Tab 가둠은 훅에 맡긴다 — 여기 있던 열두 줄은 `useFocusTrap` 의 것과 같은
|
|
228
256
|
내용이었는데 **고르개 문자열만 제 안에 따로 박혀 있었다**(공백 없는
|
|
229
257
|
`FOCUSABLE_SELECTORS`). 훅 머리말이 "네 곳을 모았다"고 적어 둔 그 넷 중
|
|
@@ -790,7 +818,7 @@ function TrainingPagePopupInner(props) {
|
|
|
790
818
|
catch (e) { }
|
|
791
819
|
},
|
|
792
820
|
onClose: handleResultDone,
|
|
793
|
-
})), (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", children: _jsx("div", { className: "page-popup__empty", children: t('training.result.eyebrow', '훈련 완료') }) }) }), _jsx("div", { className: "page-popup__bottom", children: _jsx(Button, { variant: "main", size: 48, onClick: handleResultDone, label: t('training.result.done', '완료') }) })] }) }))), _jsx(Toast, { message: noticeMessage, position: "top", onClose: handleNoticeClose })] }));
|
|
821
|
+
})), (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", 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 })] }));
|
|
794
822
|
const portalContent = (_jsx(NoisePlayerContext.Provider, { value: noiseContextValue, children: jsx }));
|
|
795
823
|
if (typeof document !== 'undefined' && document.body)
|
|
796
824
|
return createPortal(portalContent, document.body);
|
|
@@ -56,5 +56,5 @@ export default function TrainingPagePopupBottom({ useNoiseSettingBtn = false, on
|
|
|
56
56
|
setOpen(true);
|
|
57
57
|
}, [open, onSettings]);
|
|
58
58
|
const handleToggleMeters = useCallback(() => setShowMeters(prev => !prev), []);
|
|
59
|
-
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, { size: 56, type: "bar", icon: 'settings', onClick: handleDesktopClick, selected: open, 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, { size: 56, type: "dot", icon: 'settings', onClick: handleMobileClick, selected: open, ariaLabel: t('training.settings', '설정'), minWidth: 52 }) }) })] })) : null, useNoiseSettingBtn && (_jsx(Button, { size: 40, type: "dot", shape: "pill", icon: "audio-lines", onClick: handleToggleMeters, selected: showMeters, ariaLabel: t('training.meter.toggle', '오디오 미터 표시') })), useNoiseSettingBtn && showMeters && (_jsxs("div", { className: "pp-bottom__meters", 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', '소음') })] })] }))] }), _jsxs("div", { className: "pp-bottom__right", children: [useResultBtn && (_jsx(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, { 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 }))] }));
|
|
59
|
+
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, selected: open, 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", icon: 'settings', onClick: handleMobileClick, selected: open, ariaLabel: t('training.settings', '설정'), minWidth: 52 }) }) })] })) : null, useNoiseSettingBtn && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", shape: "pill", icon: "audio-lines", onClick: handleToggleMeters, selected: showMeters, ariaLabel: t('training.meter.toggle', '오디오 미터 표시') })), useNoiseSettingBtn && showMeters && (_jsxs("div", { className: "pp-bottom__meters", 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', '소음') })] })] }))] }), _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 }))] }));
|
|
60
60
|
}
|
|
@@ -37,5 +37,5 @@ export default function TrainingPagePopupTitle({ title, subtitle, desc, depth, i
|
|
|
37
37
|
}
|
|
38
38
|
return base;
|
|
39
39
|
}, [wrapperStyle]);
|
|
40
|
-
return (_jsxs("header", { className: "pp-title training-popup-title", children: [_jsxs("div", { className: "pp-title__left", children: [resolvedBack ? (_jsx(Button, { size: 48, type: "dot", icon: "chevron-left", ariaLabel: t('common.back', '뒤로'), onClick: resolvedBack, useBorder: false, iconSize: 'large', iconColor: "--color-black", variant: "transparent-gray" })) : null, _jsx("div", { children: icon ? (_jsx(CategoryIcon, { icon: icon, label: title, tooltip: iconTooltip, wrapperStyle: iconWrapperStyle })) : null }), _jsxs("div", { className: "pp-title__text", children: [resolvedDepth ? _jsx("span", { className: "pp-title__depth caption-1", children: resolvedDepth }) : null, _jsx("span", { className: `pp-title__title h2${hasName ? '' : ' pp-title__title--empty'}`, children: resolvedTitle }), desc ? _jsx("span", { className: "pp-title__description caption-1", children: desc }) : null] })] }), _jsx(ProgressIndicator, { max: Math.max(0, pageCount ?? 0), index: Math.max(0, pageIndex ?? 0) })] }));
|
|
40
|
+
return (_jsxs("header", { className: "pp-title training-popup-title", children: [_jsxs("div", { className: "pp-title__left", children: [resolvedBack ? (_jsx(Button, { htmlType: "button", size: 48, type: "dot", icon: "chevron-left", ariaLabel: t('common.back', '뒤로'), onClick: resolvedBack, useBorder: false, iconSize: 'large', iconColor: "--color-black", variant: "transparent-gray" })) : null, _jsx("div", { children: icon ? (_jsx(CategoryIcon, { icon: icon, label: title, tooltip: iconTooltip, wrapperStyle: iconWrapperStyle })) : null }), _jsxs("div", { className: "pp-title__text", children: [resolvedDepth ? _jsx("span", { className: "pp-title__depth caption-1", children: resolvedDepth }) : null, _jsx("span", { className: `pp-title__title h2${hasName ? '' : ' pp-title__title--empty'}`, children: resolvedTitle }), desc ? _jsx("span", { className: "pp-title__description caption-1", children: desc }) : null] })] }), _jsx(ProgressIndicator, { max: Math.max(0, pageCount ?? 0), index: Math.max(0, pageIndex ?? 0) })] }));
|
|
41
41
|
}
|
|
@@ -163,8 +163,18 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
163
163
|
const multiSources = Array.isArray(sources) && sources.length > 0 ? sources : (Array.isArray(segments) && segments.length > 0 ? segments : null);
|
|
164
164
|
const sourceButtonCount = Math.max(Array.isArray(multiSources) ? multiSources.length : 0, requestedSources.length);
|
|
165
165
|
// resolveMapping 제거됨 — JSON 파일 의존성 없이 resolveSoundPath(code, bucketPath) 로 직접 경로 결정
|
|
166
|
+
/* 떠났나 — 불러오기(주소 받기·길이 재기·canplay·파형 대기)는 몇 초 걸리는데 그 사이 다음 문항으로 넘기거나
|
|
167
|
+
팝업을 닫아도 흐름이 계속 돌아, **떼어진 <audio> 에서 play()** 가 불려 이전 문장이 조작 단추 없이 끝까지
|
|
168
|
+
울렸다. 재생 직전에 본다. */
|
|
169
|
+
const mountedRef = useRef(true);
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
mountedRef.current = true;
|
|
172
|
+
return () => { mountedRef.current = false; };
|
|
173
|
+
}, []);
|
|
166
174
|
const tryPlayWithRetry = useCallback(async (a, attempts = 3, delayMs = 50) => {
|
|
167
175
|
for (let i = 0; i < attempts; i++) {
|
|
176
|
+
if (!mountedRef.current)
|
|
177
|
+
return false;
|
|
168
178
|
try {
|
|
169
179
|
await a.play();
|
|
170
180
|
return true;
|
|
@@ -485,13 +495,14 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
485
495
|
setIsFetchingUrl(false);
|
|
486
496
|
}
|
|
487
497
|
};
|
|
498
|
+
/** 소스를 하나라도 얻었으면 true, 못 얻었으면 false, 이미 다른 불러오기가 돌면 null. */
|
|
488
499
|
const fetchAndPrepare = async (playAfterLoad = false, showSpinner = true) => {
|
|
489
500
|
const a = audioRef.current;
|
|
490
501
|
const candidates = soundSrc ? (Array.isArray(soundSrc) ? soundSrc : [soundSrc]) : [];
|
|
491
502
|
if (!candidates || candidates.length === 0 || !candidates.some(Boolean))
|
|
492
|
-
return;
|
|
503
|
+
return false;
|
|
493
504
|
if (fetchingRef.current)
|
|
494
|
-
return;
|
|
505
|
+
return null;
|
|
495
506
|
fetchingRef.current = true;
|
|
496
507
|
if (showSpinner)
|
|
497
508
|
setIsFetchingUrl(true);
|
|
@@ -591,6 +602,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
591
602
|
setCurrentIndex(0);
|
|
592
603
|
set구간표시(true);
|
|
593
604
|
}
|
|
605
|
+
return finalSources.length > 0;
|
|
594
606
|
}
|
|
595
607
|
finally {
|
|
596
608
|
fetchingRef.current = false;
|
|
@@ -705,7 +717,16 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
705
717
|
catch (e) { }
|
|
706
718
|
}
|
|
707
719
|
setStateLocal("load");
|
|
708
|
-
await fetchAndPrepare(true);
|
|
720
|
+
const 얻음 = await fetchAndPrepare(true);
|
|
721
|
+
/* 쓸 소스를 하나도 못 얻으면(경로를 못 정한 코드뿐 등) 'load' 에 갇혀 듣기 단추가 사라지고 아무 조작도
|
|
722
|
+
안 됐다 — 알리고 누를 수 있는 상태로 되돌린다. */
|
|
723
|
+
if (얻음 === false && mountedRef.current) {
|
|
724
|
+
try {
|
|
725
|
+
setNoticeMessage(t('training.audioFetchError', '사운드 소스를 가져오지 못했습니다.'));
|
|
726
|
+
}
|
|
727
|
+
catch (_) { }
|
|
728
|
+
setStateLocal('active');
|
|
729
|
+
}
|
|
709
730
|
return;
|
|
710
731
|
}
|
|
711
732
|
if (stateLocal === "load")
|
|
@@ -1009,10 +1030,10 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
1009
1030
|
const fillClass = cx('audio-player__fill', fillVisibleFlag ? 'is-visible' : '', fillExpandedFlag ? 'is-expanded' : '', fillPausedFlag ? 'is-paused' : '', isFillOutAnimating ? 'is-out' : '');
|
|
1010
1031
|
return (_jsxs("div", { className: `audio-player ${isDisabled ? 'is-disabled' : ''}${className ? ` ${className}` : ''}`, role: "group", "aria-label": typeof label === 'string' ? label : undefined, children: [_jsxs("div", { className: `audio-player__panel ${displayedPlaying ? "is-playing" : ""} ${isFetchingUrl ? "is-loading" : ""} ${stateLocal !== "default" ? "is-revealed" : ""}`, children: [isFetchingUrl ? (_jsxs("div", { className: "audio-player__loading", role: "status", "aria-live": "polite", children: [_jsx("span", { className: "audio-player__loading-spinner", "aria-hidden": "true" }), _jsx("span", { className: "audio-player__loading-text", children: STATUS_MESSAGES[fetchDebugStatus] })] })) : null, stateLocal === "active" ? (_jsx("button", { className: "audio-player__button", type: "button", onClick: () => { hideListenTip(); handleToggle(); }, "aria-label": typeof label === 'string' ? label : t('training.listen', '듣기'), onMouseEnter: showListenTip, onMouseLeave: hideListenTip,
|
|
1011
1032
|
// 키보드로 이 버튼에 오면 마우스와 동일하게 안내를 띄운다
|
|
1012
|
-
onFocus: showListenTip, onBlur: hideListenTip, children: _jsx("span", { ref: listenBtnRef, className: "audio-player__button-triangle-wrap", children: _jsx("svg", { className: "audio-player__button-triangle", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { ref: triPathRef, d: "M13.2 7.7 L16.5 9.85 Q20.2 12 16.5 14.15 L10 18.3 Q7 19.7 7 16.3 L7 7.7 Q7 4.3 10 5.7 L13.2 7.7" }) }) }) })) : null, listenBtnRect && (_jsx(InfoTooltip, { anchorRect: listenBtnRect, text: typeof label === 'string' && label ? label : t('training.listenLabel', '문장 듣기'), offset: 8 })), _jsxs("div", { className: `audio-player__play ${stateLocal === "playMode" || displayedPlaying ? "is-visible" : ""}`, children: [_jsxs("div", { ref: btnsRef, className: "audio-player__buttons", children: [_jsx(Button, { size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "skip-back", onClick: handlePrev, ariaLabel: t('training.prev', '이전'), width: "60px", variant: "transparent-white", "aria-hidden": "true", tabIndex: -1, disabled: stateLocal !== 'playMode' }), _jsx(Button, { size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: displayedPlaying ? "pause" : "play", onClick: handleToggle, ariaLabel: t('common.playPause', '재생 / 멈춤'), width: "60px", variant: "transparent-white", disabled: stateLocal !== 'playMode' }), _jsx(Button, { size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "square", onClick: handleStop, ariaLabel: t('training.stop', '정지'), width: "60px", variant: "transparent-white", "aria-hidden": "true", tabIndex: -1, disabled: stateLocal !== 'playMode' }), sourceButtonCount > 1 ? (_jsx("div", { ref: sourceButtonsRef, className: "audio-player__source-buttons", children: Array.from({ length: sourceButtonCount }).map((_, idx) => (_jsx(Button, { size: 32, type: "dot", shape: "pill", useBorder: false, label: String(idx + 1), onClick: () => { try {
|
|
1033
|
+
onFocus: showListenTip, onBlur: hideListenTip, children: _jsx("span", { ref: listenBtnRef, className: "audio-player__button-triangle-wrap", children: _jsx("svg", { className: "audio-player__button-triangle", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { ref: triPathRef, d: "M13.2 7.7 L16.5 9.85 Q20.2 12 16.5 14.15 L10 18.3 Q7 19.7 7 16.3 L7 7.7 Q7 4.3 10 5.7 L13.2 7.7" }) }) }) })) : null, listenBtnRect && (_jsx(InfoTooltip, { anchorRect: listenBtnRect, text: typeof label === 'string' && label ? label : t('training.listenLabel', '문장 듣기'), offset: 8 })), _jsxs("div", { className: `audio-player__play ${stateLocal === "playMode" || displayedPlaying ? "is-visible" : ""}`, children: [_jsxs("div", { ref: btnsRef, className: "audio-player__buttons", children: [_jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "skip-back", onClick: handlePrev, ariaLabel: t('training.prev', '이전'), width: "60px", variant: "transparent-white", "aria-hidden": "true", tabIndex: -1, disabled: stateLocal !== 'playMode' }), _jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: displayedPlaying ? "pause" : "play", onClick: handleToggle, ariaLabel: t('common.playPause', '재생 / 멈춤'), width: "60px", variant: "transparent-white", disabled: stateLocal !== 'playMode' }), _jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "square", onClick: handleStop, ariaLabel: t('training.stop', '정지'), width: "60px", variant: "transparent-white", "aria-hidden": "true", tabIndex: -1, disabled: stateLocal !== 'playMode' }), sourceButtonCount > 1 ? (_jsx("div", { ref: sourceButtonsRef, className: "audio-player__source-buttons", children: Array.from({ length: sourceButtonCount }).map((_, idx) => (_jsx(Button, { htmlType: "button", size: 32, type: "dot", shape: "pill", useBorder: false, label: String(idx + 1), onClick: () => { try {
|
|
1013
1034
|
handleJumpTo(idx);
|
|
1014
1035
|
}
|
|
1015
|
-
catch (e) { } }, ariaLabel: t('training.sourcePlay', { n: idx + 1 }), variant: "transparent-white", selected: 구간표시 && currentIndex === idx, disabled: stateLocal !== 'playMode' || !(Array.isArray(sources) && sources[idx]), showTooltipInfo: true }, idx))) })) : null, (!Array.isArray(sources) || sources.length <= 1) ? (_jsx(Button, { size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "repeat", onClick: handleReplay, ariaLabel: t('common.replay', '다시듣기'), width: "60px", variant: "transparent-white", disabled: stateLocal !== 'playMode' })) : null, _jsx(Button, { size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "skip-forward", onClick: handleNext, ariaLabel: t('training.next', '다음'), width: "60px", variant: "transparent-white", "aria-hidden": true, tabIndex: -1, disabled: stateLocal !== 'playMode' }), _jsx(Button, { size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "check", onClick: handleEnd, ariaLabel: t('common.done', '완료'), width: "60px", variant: "transparent-white", disabled: stateLocal !== 'playMode' })] }), _jsxs("div", { className: "audio-player__slider", children: [_jsx("div", { className: fillClass, ref: fillRef, style: { left: `${fillLeftPercent}%` }, "aria-hidden": "true" }), (showSoundWaveForm || showNoiseWaveForm) && (_jsx(WaveForm, { segments: currentSegments, progress: progress, totalDuration: totalDuration, onSegmentClick: handleJumpTo, waveProgressRef: waveProgressRef, showSoundWaveForm: showSoundWaveForm, noiseSrc: (showNoiseWaveForm && noisePlayer?.settings.enabled && noiseVolume > 0) ? activeNoiseUrl : null, noiseRatio: noisePlayer ? noiseRatioOf(noisePlayer.settings) : 0, noiseSpeed: noisePlayer?.settings.speed ?? 1,
|
|
1036
|
+
catch (e) { } }, ariaLabel: t('training.sourcePlay', { n: idx + 1 }), variant: "transparent-white", selected: 구간표시 && currentIndex === idx, disabled: stateLocal !== 'playMode' || !(Array.isArray(sources) && sources[idx]), showTooltipInfo: true }, idx))) })) : null, (!Array.isArray(sources) || sources.length <= 1) ? (_jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "repeat", onClick: handleReplay, ariaLabel: t('common.replay', '다시듣기'), width: "60px", variant: "transparent-white", disabled: stateLocal !== 'playMode' })) : null, _jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "skip-forward", onClick: handleNext, ariaLabel: t('training.next', '다음'), width: "60px", variant: "transparent-white", "aria-hidden": true, tabIndex: -1, disabled: stateLocal !== 'playMode' }), _jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "check", onClick: handleEnd, ariaLabel: t('common.done', '완료'), width: "60px", variant: "transparent-white", disabled: stateLocal !== 'playMode' })] }), _jsxs("div", { className: "audio-player__slider", children: [_jsx("div", { className: fillClass, ref: fillRef, style: { left: `${fillLeftPercent}%` }, "aria-hidden": "true" }), (showSoundWaveForm || showNoiseWaveForm) && (_jsx(WaveForm, { segments: currentSegments, progress: progress, totalDuration: totalDuration, onSegmentClick: handleJumpTo, waveProgressRef: waveProgressRef, showSoundWaveForm: showSoundWaveForm, noiseSrc: (showNoiseWaveForm && noisePlayer?.settings.enabled && noiseVolume > 0) ? activeNoiseUrl : null, noiseRatio: noisePlayer ? noiseRatioOf(noisePlayer.settings) : 0, noiseSpeed: noisePlayer?.settings.speed ?? 1,
|
|
1016
1037
|
/* 그림은 "문장 = 기준" — 문제 소리(M÷R)가 아니라 전체 소리 M 으로 문장을 그리고 소음을 R 배로 키운다 */
|
|
1017
1038
|
mainVolume: mainVolume, onReadyChange: handleWaveReadyChange }))] })] }), _jsx("audio", { ref: audioRef, crossOrigin: "anonymous", preload: "auto" })] }), !showSoundWaveForm && (_jsxs("p", { className: "audio-player__no-waveform caption-1", children: [_jsx(Icon, { icon: "triangle-alert", size: 14 }), t('training.noSoundWaveform', '소리 비교 문제는 웨이브폼을 표현하지 않습니다.')] })), _jsx(Toast, { message: noticeMessage, position: noticePosition, onClose: handleClearNotice })] }));
|
|
1018
1039
|
}
|
|
@@ -113,5 +113,5 @@ export default function TrainingStepConfirm({ index, stepIndex, isLast = false,
|
|
|
113
113
|
때문이다 — 나머지 둘(choices 바뀜·페이지 전환)은 빈 Set 으로 되돌리는 자리라
|
|
114
114
|
저 effect 의 `size === 0` 가드에 걸려 어차피 아무것도 안 했다. */
|
|
115
115
|
const btnsClassName = `training-step__buttons${Array.isArray(choices) && choices.length > 3 ? ' training-step__buttons--wrap-3' : ''}`;
|
|
116
|
-
return (_jsx(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, title: resolvedTitle, useTitleFaded: useTitleFaded, children: _jsx("div", { className: btnsClassName, children: Array.isArray(choices) && choices.length > 0 ? (choices.map((c, i) => (_jsx(Button, { size: 56, type: "bar", onClick: () => handleSelect(i), ariaLabel: String(c), label: c, useLabel: true, icon: choiceIcons ? choiceIcons[String(c)] : undefined, variant: 'default', disabled: currentState === 'disabled', selected: multiSelect ? selectedSet.has(c) : selectedAnswer === c, useDisabledCovered: useDisabledCovered, minWidth: 0 }, String(c))))) : null }) }));
|
|
116
|
+
return (_jsx(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, title: resolvedTitle, useTitleFaded: useTitleFaded, children: _jsx("div", { className: btnsClassName, children: Array.isArray(choices) && choices.length > 0 ? (choices.map((c, i) => (_jsx(Button, { htmlType: "button", size: 56, type: "bar", onClick: () => handleSelect(i), ariaLabel: String(c), label: c, useLabel: true, icon: choiceIcons ? choiceIcons[String(c)] : undefined, variant: 'default', disabled: currentState === 'disabled', selected: multiSelect ? selectedSet.has(c) : selectedAnswer === c, useDisabledCovered: useDisabledCovered, minWidth: 0 }, String(c))))) : null }) }));
|
|
117
117
|
}
|
|
@@ -49,11 +49,11 @@ export default function TrainingStepConfirmWithText({ index, stepIndex, isLast =
|
|
|
49
49
|
setSelectedAnswer(true);
|
|
50
50
|
handleEndedLocal(true);
|
|
51
51
|
}, [handleEndedLocal, currentState]);
|
|
52
|
-
return (_jsxs(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, title: resolvedTitle, onDone: onDone, useTitleFaded: false, children: [_jsx(TrainingStepTextBox, { context: context, state: currentState }), _jsx("div", { className: "training-step__buttons", children: Array.isArray(choices) && choices.length > 0 ? (choices.slice(0, 6).map((c, i) => (_jsx(Button, { size: 56, type: "bar", onClick: () => {
|
|
52
|
+
return (_jsxs(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, title: resolvedTitle, onDone: onDone, useTitleFaded: false, children: [_jsx(TrainingStepTextBox, { context: context, state: currentState }), _jsx("div", { className: "training-step__buttons", children: Array.isArray(choices) && choices.length > 0 ? (choices.slice(0, 6).map((c, i) => (_jsx(Button, { htmlType: "button", size: 56, type: "bar", onClick: () => {
|
|
53
53
|
if (currentState !== 'active')
|
|
54
54
|
return;
|
|
55
55
|
setSelectedAnswer(i);
|
|
56
56
|
if (handleEndedLocal)
|
|
57
57
|
handleEndedLocal(i);
|
|
58
|
-
}, disabled: currentState === 'disabled', ariaLabel: String(c), label: c, variant: currentState, selected: selectedAnswer === i }, i)))) : (_jsxs(_Fragment, { children: [_jsx(Button, { size: 56, type: "bar", onClick: handleSelectFalse, disabled: currentState === 'disabled', icon: 'x', ariaLabel: t('training.notHeard', '안 들렸어요.'), label: t('training.notHeard', '안 들렸어요.'), variant: 'default', selected: selectedAnswer === false }), _jsx(Button, { size: 56, type: "bar", onClick: handleSelectTrue, disabled: currentState === 'disabled', icon: 'circle', ariaLabel: t('training.heardWell', '잘 들렸어요.'), label: t('training.heardWell', '잘 들렸어요.'), variant: 'default', selected: selectedAnswer === true })] })) })] }));
|
|
58
|
+
}, disabled: currentState === 'disabled', ariaLabel: String(c), label: c, variant: currentState, selected: selectedAnswer === i }, i)))) : (_jsxs(_Fragment, { children: [_jsx(Button, { htmlType: "button", size: 56, type: "bar", onClick: handleSelectFalse, disabled: currentState === 'disabled', icon: 'x', ariaLabel: t('training.notHeard', '안 들렸어요.'), label: t('training.notHeard', '안 들렸어요.'), variant: 'default', selected: selectedAnswer === false }), _jsx(Button, { htmlType: "button", size: 56, type: "bar", onClick: handleSelectTrue, disabled: currentState === 'disabled', icon: 'circle', ariaLabel: t('training.heardWell', '잘 들렸어요.'), label: t('training.heardWell', '잘 들렸어요.'), variant: 'default', selected: selectedAnswer === true })] })) })] }));
|
|
59
59
|
}
|
|
@@ -440,6 +440,6 @@ export default function TrainingStepOrderList({ index, stepIndex, isLast = false
|
|
|
440
440
|
}, 80);
|
|
441
441
|
}
|
|
442
442
|
catch (err) { }
|
|
443
|
-
} : undefined, onDrop: currentState === 'active' ? (e) => handleDrop(idx, e) : undefined, onDragEnd: currentState === 'active' ? handleDragEnd : undefined, children: [_jsx("span", { className: "order-list__item-text body-2", children: it.text }), currentState === 'active' ? (_jsxs("span", { className: "order-list__item-buttons", children: [_jsx(Button, { size: 32, type: "dot", onClick: () => move(idx, -1), ariaLabel: t("training.up"), icon: 'chevron-up', variant: 'transparent-gray', width: 48, useFaded: "true", disabled: idx === 0, useBorder: false, useDisabledInvisible: true }), _jsx(Button, { size: 32, type: "dot", onClick: () => move(idx, 1), ariaLabel: t("training.down"), icon: 'chevron-down', variant: 'transparent-gray', width: 48, useFaded: "true", disabled: idx === shuffled.length - 1, useBorder: false, useDisabledInvisible: true })] })) : null] })] }) }, it.id));
|
|
443
|
+
} : undefined, onDrop: currentState === 'active' ? (e) => handleDrop(idx, e) : undefined, onDragEnd: currentState === 'active' ? handleDragEnd : undefined, children: [_jsx("span", { className: "order-list__item-text body-2", children: it.text }), currentState === 'active' ? (_jsxs("span", { className: "order-list__item-buttons", children: [_jsx(Button, { htmlType: "button", size: 32, type: "dot", onClick: () => move(idx, -1), ariaLabel: t("training.up"), icon: 'chevron-up', variant: 'transparent-gray', width: 48, useFaded: "true", disabled: idx === 0, useBorder: false, useDisabledInvisible: true }), _jsx(Button, { htmlType: "button", size: 32, type: "dot", onClick: () => move(idx, 1), ariaLabel: t("training.down"), icon: 'chevron-down', variant: 'transparent-gray', width: 48, useFaded: "true", disabled: idx === shuffled.length - 1, useBorder: false, useDisabledInvisible: true })] })) : null] })] }) }, it.id));
|
|
444
444
|
}) }) }));
|
|
445
445
|
}
|
|
@@ -70,5 +70,5 @@ export default function TrainingStepTextBox({ context, state = "disabled", theme
|
|
|
70
70
|
fontSize: `${fontSize}px`,
|
|
71
71
|
lineHeight: `${lineHeight}px`,
|
|
72
72
|
}), [fontSize, lineHeight]);
|
|
73
|
-
return (_jsxs("div", { className: `training-text-box ${stateClass} ${displayStatus !== 'disabled' && theme === 'dark' ? 'is-dark' : ''}`, children: [displayStatus === "disabled" && (_jsx("div", { className: "training-text-box__preview", children: _jsx(Button, { size: 48, type: "bar", onClick: handleActivate, icon: 'eye', ariaLabel: t('training.sentencePreview', '문장 미리보기'), label: t('training.sentencePreview', '문장 미리보기'), useBorder: false }) })), _jsx("div", { className: "training-text-box__area", "aria-hidden": displayStatus === "disabled", children: _jsx("span", { className: "training-text-box__inner h3", style: textStyle, children: content }) }), displayStatus !== "disabled" && (_jsxs("div", { className: "training-text-box__buttons", "aria-hidden": displayStatus === "disabled", children: [_jsx(Button, { size: 32, type: "dot", onClick: decrease, onDisabledClick: () => showNotice(t('training.fontSizeMin', '글씨 크기가 최소입니다.')), ariaLabel: t('training.fontSizeDecrease', '글자 크기 감소'), icon: 'minus', variant: theme === 'dark' ? 'transparent-white' : 'transparent-gray', disabled: !canDecrease, useBorder: false, showTooltipInfo: true, ...(!isMobile && { width: '60px' }) }), _jsx(Button, { size: 32, type: "dot", onClick: increase, onDisabledClick: () => showNotice(t('training.fontSizeMax', '글씨 크기가 최대입니다.')), ariaLabel: t('training.fontSizeIncrease', '글자 크기 증가'), icon: 'plus', variant: theme === 'dark' ? 'transparent-white' : 'transparent-gray', disabled: !canIncrease, useBorder: false, showTooltipInfo: true, ...(!isMobile && { width: '60px' }) }), _jsx(Button, { size: 32, showTooltipInfo: true, type: "dot", useBorder: false, width: 48, icon: theme === 'dark' ? 'moon' : 'sun-medium', onClick: toggleTheme, ariaLabel: t('training.toggleBackground', '배경 전환'), variant: theme === 'dark' ? 'transparent-white' : 'transparent-gray' })] })), _jsx(Toast, { message: noticeMessage, position: "top", onClose: handleNoticeClose })] }));
|
|
73
|
+
return (_jsxs("div", { className: `training-text-box ${stateClass} ${displayStatus !== 'disabled' && theme === 'dark' ? 'is-dark' : ''}`, children: [displayStatus === "disabled" && (_jsx("div", { className: "training-text-box__preview", children: _jsx(Button, { htmlType: "button", size: 48, type: "bar", onClick: handleActivate, icon: 'eye', ariaLabel: t('training.sentencePreview', '문장 미리보기'), label: t('training.sentencePreview', '문장 미리보기'), useBorder: false }) })), _jsx("div", { className: "training-text-box__area", "aria-hidden": displayStatus === "disabled", children: _jsx("span", { className: "training-text-box__inner h3", style: textStyle, children: content }) }), displayStatus !== "disabled" && (_jsxs("div", { className: "training-text-box__buttons", "aria-hidden": displayStatus === "disabled", children: [_jsx(Button, { size: 32, type: "dot", onClick: decrease, onDisabledClick: () => showNotice(t('training.fontSizeMin', '글씨 크기가 최소입니다.')), ariaLabel: t('training.fontSizeDecrease', '글자 크기 감소'), icon: 'minus', variant: theme === 'dark' ? 'transparent-white' : 'transparent-gray', disabled: !canDecrease, useBorder: false, showTooltipInfo: true, ...(!isMobile && { width: '60px' }) }), _jsx(Button, { size: 32, type: "dot", onClick: increase, onDisabledClick: () => showNotice(t('training.fontSizeMax', '글씨 크기가 최대입니다.')), ariaLabel: t('training.fontSizeIncrease', '글자 크기 증가'), icon: 'plus', variant: theme === 'dark' ? 'transparent-white' : 'transparent-gray', disabled: !canIncrease, useBorder: false, showTooltipInfo: true, ...(!isMobile && { width: '60px' }) }), _jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", useBorder: false, width: 48, icon: theme === 'dark' ? 'moon' : 'sun-medium', onClick: toggleTheme, ariaLabel: t('training.toggleBackground', '배경 전환'), variant: theme === 'dark' ? 'transparent-white' : 'transparent-gray' })] })), _jsx(Toast, { message: noticeMessage, position: "top", onClose: handleNoticeClose })] }));
|
|
74
74
|
}
|
|
@@ -87,5 +87,5 @@ export default function TrainingStepTitle({ title, children, className = "", sta
|
|
|
87
87
|
setPreviewContent(null);
|
|
88
88
|
}
|
|
89
89
|
}, [onPreviewTextBtn, previewText, showPreview]);
|
|
90
|
-
return (_jsxs("div", { className: rowClass, children: [_jsx("p", { className: cls, children: _jsx("span", { className: "training-step__title-text", children: title ?? children }) }), (usePreviewTextBtn || usePlayAllBtn) ? (_jsxs("div", { className: "title-controls", children: [_jsxs("div", { className: "title-buttons", children: [usePlayAllBtn && (_jsx(Button, { size: 32, type: isMobile ? 'dot' : 'bar', icon: "play-circle", label: isMobile ? undefined : t('training.playAll', '모두 듣기'), ariaLabel: t('training.playAll', '모두 듣기'), selected: playAllActive, onClick: onPlayAll, variant: state === 'disabled' ? 'disabled' : 'default', disabled: state === 'disabled' || playAllActive })), usePreviewTextBtn && (_jsx("div", { ref: btnRef, children: _jsx(Button, { size: 32, type: isMobile ? 'dot' : 'bar', icon: "eye", label: isMobile ? undefined : t('training.sentencePreview', '문장 미리보기'), ariaLabel: t('training.sentencePreview', '문장 미리보기'), selected: showPreview, onClick: handlePreviewButtonClick, variant: state === 'disabled' ? 'disabled' : 'default', disabled: !previewText && typeof onPreviewTextBtn !== 'function' }) }))] }), showPreview && (_jsx(TextBoxTooltip, { text: (previewContent ?? previewText), label: t('training.sentencePreview', '문장 미리보기'), anchorRect: anchorRect, onClose: handlePreviewClose, closeOnOutsideClick: false, isMobilePopup: true }))] })) : null] }));
|
|
90
|
+
return (_jsxs("div", { className: rowClass, children: [_jsx("p", { className: cls, children: _jsx("span", { className: "training-step__title-text", children: title ?? children }) }), (usePreviewTextBtn || usePlayAllBtn) ? (_jsxs("div", { className: "title-controls", children: [_jsxs("div", { className: "title-buttons", children: [usePlayAllBtn && (_jsx(Button, { htmlType: "button", size: 32, type: isMobile ? 'dot' : 'bar', icon: "play-circle", label: isMobile ? undefined : t('training.playAll', '모두 듣기'), ariaLabel: t('training.playAll', '모두 듣기'), selected: playAllActive, onClick: onPlayAll, variant: state === 'disabled' ? 'disabled' : 'default', disabled: state === 'disabled' || playAllActive })), usePreviewTextBtn && (_jsx("div", { ref: btnRef, children: _jsx(Button, { htmlType: "button", size: 32, type: isMobile ? 'dot' : 'bar', icon: "eye", label: isMobile ? undefined : t('training.sentencePreview', '문장 미리보기'), ariaLabel: t('training.sentencePreview', '문장 미리보기'), selected: showPreview, onClick: handlePreviewButtonClick, variant: state === 'disabled' ? 'disabled' : 'default', disabled: !previewText && typeof onPreviewTextBtn !== 'function' }) }))] }), showPreview && (_jsx(TextBoxTooltip, { text: (previewContent ?? previewText), label: t('training.sentencePreview', '문장 미리보기'), anchorRect: anchorRect, onClose: handlePreviewClose, closeOnOutsideClick: false, isMobilePopup: true }))] })) : null] }));
|
|
91
91
|
}
|