@ihabdevteam/core 0.322.0 → 0.324.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 +557 -0
- package/dist/components.cjs.js +183 -183
- package/dist/components.esm.css +1 -1
- package/dist/hooks.cjs.js +3 -3
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core.cjs.js +187 -187
- package/dist/ihabdevteam-core.d.ts +55 -17
- package/dist/packages/core/src/components/Accordion.js +3 -3
- package/dist/packages/core/src/components/Alert.js +2 -2
- package/dist/packages/core/src/components/BarChart.js +120 -119
- package/dist/packages/core/src/components/BottomNav.js +18 -18
- package/dist/packages/core/src/components/BulkBar.js +25 -25
- package/dist/packages/core/src/components/Button.js +16 -16
- package/dist/packages/core/src/components/Calendar.js +17 -17
- package/dist/packages/core/src/components/ChartSection.js +30 -30
- package/dist/packages/core/src/components/CountTile.js +38 -38
- package/dist/packages/core/src/components/DatePicker.js +59 -59
- package/dist/packages/core/src/components/Dialog.js +14 -14
- package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.js +8 -8
- package/dist/packages/core/src/components/FilterPanel.js +6 -6
- package/dist/packages/core/src/components/FilterSectionInfo.js +6 -6
- package/dist/packages/core/src/components/GameIntro.js +1 -1
- package/dist/packages/core/src/components/Icon.js +2 -2
- package/dist/packages/core/src/components/ItemActions.js +16 -16
- package/dist/packages/core/src/components/LandingHeader.js +6 -6
- package/dist/packages/core/src/components/LineChart.js +24 -24
- package/dist/packages/core/src/components/Loader.js +5 -5
- package/dist/packages/core/src/components/NoticeBox.js +3 -3
- package/dist/packages/core/src/components/PageTitle.js +2 -2
- package/dist/packages/core/src/components/Pagination.js +4 -4
- package/dist/packages/core/src/components/Panel.js +2 -2
- package/dist/packages/core/src/components/PanelRow.js +6 -13
- package/dist/packages/core/src/components/PassCard/PassCard.js +9 -9
- package/dist/packages/core/src/components/PassCard/passCardProduct.js +2 -2
- package/dist/packages/core/src/components/PaymentPlan.js +5 -5
- package/dist/packages/core/src/components/Popup/ConfirmPopup.js +29 -29
- package/dist/packages/core/src/components/Popup/CouponInputPopup.js +2 -2
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +18 -25
- package/dist/packages/core/src/components/Popup/ListPopup.js +3 -3
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +7 -7
- package/dist/packages/core/src/components/Popup/Popup.js +27 -27
- package/dist/packages/core/src/components/Popup/SaveTablePopup.js +5 -5
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +4 -4
- package/dist/packages/core/src/components/Popup/customerDetailUtils.js +18 -18
- package/dist/packages/core/src/components/Popup/useDiscardGuard.js +4 -4
- package/dist/packages/core/src/components/ProgressIndicator.js +5 -5
- package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +16 -16
- package/dist/packages/core/src/components/Rating.js +20 -20
- package/dist/packages/core/src/components/Report/ReportDocument.js +21 -21
- package/dist/packages/core/src/components/RingChart.js +5 -5
- package/dist/packages/core/src/components/SectionHeadFloat.js +15 -15
- package/dist/packages/core/src/components/SegmentedControl.js +34 -34
- package/dist/packages/core/src/components/SideNav.js +37 -37
- package/dist/packages/core/src/components/Slider.js +2 -2
- package/dist/packages/core/src/components/SoundCompass.js +5 -5
- package/dist/packages/core/src/components/Table.js +257 -98
- package/dist/packages/core/src/components/Tabs.js +12 -12
- package/dist/packages/core/src/components/TextField.js +34 -34
- package/dist/packages/core/src/components/Toast.js +6 -6
- package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +4 -4
- package/dist/packages/core/src/components/Tooltip/Tooltip.js +22 -22
- package/dist/packages/core/src/components/Tooltip/TrainingTooltip.js +4 -4
- package/dist/packages/core/src/components/TopBanner.js +28 -28
- package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +11 -11
- package/dist/packages/core/src/components/Training/CategoryIcon.js +2 -2
- package/dist/packages/core/src/components/Training/NoisePlayerContext.js +12 -12
- package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingBlock.js +11 -11
- package/dist/packages/core/src/components/Training/TrainingBlockButton.js +10 -10
- package/dist/packages/core/src/components/Training/TrainingBlockGrid.js +21 -21
- package/dist/packages/core/src/components/Training/TrainingCard.css +13 -1
- package/dist/packages/core/src/components/Training/TrainingCard.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingCardGroup.js +6 -6
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +81 -81
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +17 -17
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +127 -127
- package/dist/packages/core/src/components/Training/TrainingStepCard.js +18 -18
- package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +4 -4
- package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +12 -12
- package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +12 -12
- package/dist/packages/core/src/components/Training/TrainingStepTitle.js +4 -4
- package/dist/packages/core/src/components/Training/TrainingWordCard.js +17 -17
- package/dist/packages/core/src/components/Training/WordTablePopup.js +5 -5
- package/dist/packages/core/src/components/Training/audioPlaybackRules.js +8 -8
- package/dist/packages/core/src/components/Training/audioTimeline.js +25 -25
- package/dist/packages/core/src/components/Training/oneShotSound.js +13 -13
- package/dist/packages/core/src/components/Training/trainingMissStats.js +20 -20
- package/dist/packages/core/src/components/Training/trainingPopupUtils.js +7 -7
- package/dist/packages/core/src/components/Training/useAudioPreview.js +15 -15
- package/dist/packages/core/src/components/Training/useFloatingFocus.js +14 -14
- package/dist/packages/core/src/components/Training/useNoiseFade.js +22 -22
- package/dist/packages/core/src/components/Training/useObjectUrls.js +2 -2
- package/dist/packages/core/src/components/Training/usePlaybackSequencer.d.ts +2 -2
- package/dist/packages/core/src/components/Training/usePlaybackSequencer.js +23 -23
- package/dist/packages/core/src/components/Training/useProgressTicker.js +33 -33
- package/dist/packages/core/src/components/Training/useTTSPlayback.js +17 -17
- package/dist/packages/core/src/components/Training/useWaveReady.js +11 -11
- package/dist/packages/core/src/components/UpdateAlert.js +6 -6
- package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +7 -1
- package/dist/packages/core/src/components/Word/TableFilterPanel.js +3 -3
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +10 -10
- package/dist/packages/core/src/components/Word/WordTableBlock.js +28 -28
- package/dist/packages/core/src/components/Word/WordTableButton.css +16 -2
- package/dist/packages/core/src/components/Word/WordTableThumbnail.css +6 -1
- package/dist/packages/core/src/components/Word/generatedDraft.js +14 -14
- package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -7
- package/dist/packages/core/src/components/Word/wordScoreStats.js +15 -15
- package/dist/packages/core/src/components/Word/wordTablePrint.js +10 -10
- package/dist/packages/core/src/components/Word/wordTableRecommend.js +11 -11
- package/dist/packages/core/src/components/Word/wordTableReview.js +28 -28
- package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +8 -11
- package/dist/packages/core/src/components/Word/wordTableSettings.js +48 -36
- package/dist/packages/core/src/components/WordTable.js +130 -130
- package/dist/packages/core/src/components/barChartData.d.ts +16 -1
- package/dist/packages/core/src/components/barChartData.js +49 -29
- package/dist/packages/core/src/components/barChartTypes.js +8 -8
- package/dist/packages/core/src/components/tableTrackList.js +11 -11
- package/dist/packages/core/src/components/updateAlertStack.js +28 -28
- package/dist/packages/core/src/components/useBarChartControl.js +4 -6
- package/dist/packages/core/src/components/useComboboxSuggestions.js +17 -17
- package/dist/packages/core/src/components/useToastLifecycle.js +18 -18
- package/dist/packages/core/src/components/wordTableLayout.d.ts +17 -4
- package/dist/packages/core/src/components/wordTableLayout.js +5 -5
- package/dist/packages/core/src/context/I18nProvider.js +6 -6
- package/dist/packages/core/src/hooks/focusReturn.js +27 -27
- package/dist/packages/core/src/hooks/inputModality.js +13 -13
- package/dist/packages/core/src/hooks/touchToggle.js +39 -39
- package/dist/packages/core/src/hooks/useAnchoredMenu.js +30 -30
- package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +28 -28
- package/dist/packages/core/src/hooks/useDocumentTitle.js +10 -10
- package/dist/packages/core/src/hooks/useEditableList.js +4 -4
- package/dist/packages/core/src/hooks/useFocusTrap.js +2 -2
- package/dist/packages/core/src/hooks/useFontScale.js +6 -6
- package/dist/packages/core/src/hooks/useIcon.js +7 -7
- package/dist/packages/core/src/hooks/useListControls.js +2 -2
- package/dist/packages/core/src/hooks/usePageResetOnChange.js +3 -3
- package/dist/packages/core/src/hooks/usePasswordChange.js +4 -4
- package/dist/packages/core/src/hooks/usePendingPress.js +7 -7
- package/dist/packages/core/src/hooks/useRelativeDay.js +4 -4
- package/dist/packages/core/src/hooks/useSoundCompassExam.js +6 -6
- package/dist/packages/core/src/hooks/useTablePaging.d.ts +0 -11
- package/dist/packages/core/src/hooks/useTablePaging.js +29 -34
- package/dist/packages/core/src/hooks/useTrainingSessionLog.js +35 -35
- package/dist/packages/core/src/hooks/useVisitSchedule.js +4 -4
- package/dist/packages/core/src/hooks/useWordAttributeFilter.js +14 -14
- package/dist/packages/core/src/hooks/useWordMeanings.js +5 -5
- package/dist/packages/core/src/hooks/useWordTrainingFilter.js +7 -7
- package/dist/packages/core/src/i18n/locales/en.json +14 -2
- package/dist/packages/core/src/i18n/locales/ko.json +13 -1
- package/dist/packages/core/src/types/wordSearch.d.ts +9 -0
- package/dist/packages/core/src/utils/audioMeter.js +5 -5
- package/dist/packages/core/src/utils/audioPeaks.d.ts +1 -1
- package/dist/packages/core/src/utils/audioPeaks.js +18 -18
- package/dist/packages/core/src/utils/blockGrid.js +4 -4
- package/dist/packages/core/src/utils/chunkSkew.js +9 -9
- package/dist/packages/core/src/utils/clipboard.js +3 -3
- package/dist/packages/core/src/utils/dailySelectionUtils.js +12 -12
- package/dist/packages/core/src/utils/date.js +12 -12
- package/dist/packages/core/src/utils/devWarn.js +4 -4
- package/dist/packages/core/src/utils/deviceKind.d.ts +37 -7
- package/dist/packages/core/src/utils/deviceKind.js +53 -10
- package/dist/packages/core/src/utils/errorMessage.js +7 -7
- package/dist/packages/core/src/utils/filterPanelSummary.js +14 -14
- package/dist/packages/core/src/utils/format.js +4 -4
- package/dist/packages/core/src/utils/getTrainingData.js +9 -9
- package/dist/packages/core/src/utils/imageConfig.js +17 -17
- package/dist/packages/core/src/utils/koreanSyllable.js +18 -18
- package/dist/packages/core/src/utils/navScroll.js +2 -2
- package/dist/packages/core/src/utils/phonemeConvert.d.ts +7 -0
- package/dist/packages/core/src/utils/phonemeConvert.js +19 -10
- package/dist/packages/core/src/utils/phonemeGroups.js +2 -2
- package/dist/packages/core/src/utils/phonemePairSearch.d.ts +1 -0
- package/dist/packages/core/src/utils/phonemePairSearch.js +129 -105
- package/dist/packages/core/src/utils/phonemePairSearchTypes.d.ts +2 -0
- package/dist/packages/core/src/utils/phonemeSectionCodes.d.ts +52 -0
- package/dist/packages/core/src/utils/phonemeSectionCodes.js +76 -0
- package/dist/packages/core/src/utils/prefetchChunk.js +6 -6
- package/dist/packages/core/src/utils/safeHref.js +4 -4
- package/dist/packages/core/src/utils/savedWordTables.js +5 -5
- package/dist/packages/core/src/utils/speak.js +12 -12
- package/dist/packages/core/src/utils/textSort.d.ts +0 -10
- package/dist/packages/core/src/utils/textSort.js +38 -48
- package/dist/packages/core/src/utils/theme.js +3 -3
- package/dist/packages/core/src/utils/trainingItemResults.js +3 -3
- package/dist/packages/core/src/utils/ttsConfig.js +2 -2
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +1 -1
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +65 -45
- package/dist/packages/core/src/utils/wordContrast.js +3 -3
- package/dist/packages/core/src/utils/wordPairIndex.js +4 -4
- package/dist/packages/core/src/utils/wordTrainingGenerators.js +4 -4
- package/dist/packages/core/src/utils/wordTrainingPool.js +15 -15
- package/dist/packages/core/src/utils/wordTrainingRuntime.js +6 -6
- package/dist/packages/core/src/utils/wornDate.d.ts +3 -3
- package/dist/packages/core/src/utils/wornDate.js +14 -14
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +4 -4
- package/package.json +1 -1
|
@@ -8,27 +8,27 @@ import { FOCUSABLE_SELECTOR } from './useFocusTrap';
|
|
|
8
8
|
* 미리보기와 함께 사라진다. 확인 창을 닫으면 떼어진 단추에 focus() 해 아무 일도 없이 body 에 떨어졌다.
|
|
9
9
|
* 각 창의 판(panel) → 그 창이 돌려줄 곳을 적어 두고, 돌려줄 곳이 떼어졌으면 그것을 품었던 창의 돌려줄 곳을 따라 올라간다.
|
|
10
10
|
*/
|
|
11
|
-
const
|
|
11
|
+
const returnTargets = new WeakMap();
|
|
12
12
|
/** 창 판과, 그 창이 열릴 때 포커스였던 요소를 적는다 */
|
|
13
13
|
export function rememberFocusReturn(panel, previouslyFocused) {
|
|
14
14
|
if (panel)
|
|
15
|
-
|
|
15
|
+
returnTargets.set(panel, previouslyFocused);
|
|
16
16
|
}
|
|
17
17
|
/** 실제로 돌려줄 곳 — 문서에 붙어 있으면 그대로, 떼어졌으면 그것을 품었던 창이 적어 둔 곳을 따라간다 */
|
|
18
18
|
export function resolveFocusReturn(previouslyFocused) {
|
|
19
|
-
let
|
|
20
|
-
for (let
|
|
21
|
-
let
|
|
22
|
-
while (
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
return
|
|
26
|
-
|
|
19
|
+
let target = previouslyFocused;
|
|
20
|
+
for (let hop = 0; target && !target.isConnected && hop < 10; hop++) {
|
|
21
|
+
let node = target;
|
|
22
|
+
while (node && !returnTargets.has(node))
|
|
23
|
+
node = node.parentElement;
|
|
24
|
+
if (!node)
|
|
25
|
+
return target;
|
|
26
|
+
target = returnTargets.get(node) ?? null;
|
|
27
27
|
}
|
|
28
|
-
return
|
|
28
|
+
return target;
|
|
29
29
|
}
|
|
30
30
|
/* 포커스를 받을 수 있는가 — 붙어 있고, 막히지 않았고, inert·aria-hidden·hidden 아래가 아니다(레이아웃은 재지 않는다: 시험 환경에 없다) */
|
|
31
|
-
function
|
|
31
|
+
function canFocus(el) {
|
|
32
32
|
if (!el || !el.isConnected || el === document.body)
|
|
33
33
|
return false;
|
|
34
34
|
if (typeof el.focus !== 'function')
|
|
@@ -53,24 +53,24 @@ function 받을수있나(el) {
|
|
|
53
53
|
* 옮긴 요소를 돌려준다(못 옮기면 null).
|
|
54
54
|
*/
|
|
55
55
|
export function moveFocusOutside(container, opener, anchor) {
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
56
|
+
const anchorNode = container?.isConnected ? container : anchor?.isConnected ? anchor : null;
|
|
57
|
+
if (canFocus(opener) && !(container && container.contains(opener))) {
|
|
58
58
|
opener.focus();
|
|
59
59
|
return opener;
|
|
60
60
|
}
|
|
61
|
-
if (
|
|
61
|
+
if (!anchorNode)
|
|
62
62
|
return null;
|
|
63
|
-
const
|
|
64
|
-
.filter((el) => !(container && container.contains(el)) &&
|
|
65
|
-
let
|
|
66
|
-
let
|
|
67
|
-
for (const el of
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
else if (
|
|
71
|
-
|
|
63
|
+
const allFocusable = Array.from(document.querySelectorAll(FOCUSABLE_SELECTOR))
|
|
64
|
+
.filter((el) => !(container && container.contains(el)) && canFocus(el));
|
|
65
|
+
let before = null;
|
|
66
|
+
let after = null;
|
|
67
|
+
for (const el of allFocusable) {
|
|
68
|
+
if (anchorNode.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING)
|
|
69
|
+
before = el;
|
|
70
|
+
else if (!after)
|
|
71
|
+
after = el;
|
|
72
72
|
}
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
return
|
|
73
|
+
const target = before ?? after;
|
|
74
|
+
target?.focus();
|
|
75
|
+
return target;
|
|
76
76
|
}
|
|
@@ -6,26 +6,26 @@
|
|
|
6
6
|
* 초점을 옮겨 그 단추의 초점 툴팁이 떴다(듣기 → '재생 / 멈춤', 문장 미리보기 → '글자 크기 감소'). Safari 는 누른 단추에 초점을
|
|
7
7
|
* 안 줘 body 에 남는다. 문서 전체에서 키 누름·포인터 누름을 들어 마지막 것을 기억한다(캡처 단계, 한 번만 붙인다).
|
|
8
8
|
*/
|
|
9
|
-
let
|
|
10
|
-
let
|
|
11
|
-
function
|
|
12
|
-
if (
|
|
9
|
+
let last = null;
|
|
10
|
+
let attached = false;
|
|
11
|
+
function attach() {
|
|
12
|
+
if (attached || typeof document === 'undefined')
|
|
13
13
|
return;
|
|
14
|
-
|
|
14
|
+
attached = true;
|
|
15
15
|
document.addEventListener('keydown', (e) => {
|
|
16
16
|
if (e.metaKey || e.ctrlKey || e.altKey)
|
|
17
17
|
return;
|
|
18
|
-
|
|
18
|
+
last = 'keyboard';
|
|
19
19
|
}, true);
|
|
20
|
-
const
|
|
21
|
-
document.addEventListener('pointerdown',
|
|
22
|
-
document.addEventListener('mousedown',
|
|
23
|
-
document.addEventListener('touchstart',
|
|
20
|
+
const onPointer = () => { last = 'pointer'; };
|
|
21
|
+
document.addEventListener('pointerdown', onPointer, true);
|
|
22
|
+
document.addEventListener('mousedown', onPointer, true);
|
|
23
|
+
document.addEventListener('touchstart', onPointer, { capture: true, passive: true });
|
|
24
24
|
}
|
|
25
25
|
// 불러오는 즉시 듣는다 — 첫 누름부터 알아야 한다
|
|
26
|
-
|
|
26
|
+
attach();
|
|
27
27
|
/** 마지막 입력이 키보드였으면 true(모르면 false) */
|
|
28
28
|
export function wasKeyboardPress() {
|
|
29
|
-
|
|
30
|
-
return
|
|
29
|
+
attach();
|
|
30
|
+
return last === 'keyboard';
|
|
31
31
|
}
|
|
@@ -12,25 +12,25 @@ import { useLatestRef } from './useLatestRef';
|
|
|
12
12
|
* 마우스 동작은 그대로 두고, pointerType 이 touch 일 때만: 들어오고 나감은 무시 · 뗄 때 뒤집기 · 방금 톡으로 온 focus 는 무시 ·
|
|
13
13
|
* 바깥을 톡하면 닫기.
|
|
14
14
|
*/
|
|
15
|
-
let
|
|
16
|
-
let
|
|
17
|
-
function
|
|
18
|
-
if (
|
|
15
|
+
let lastTouchAt = 0;
|
|
16
|
+
let attached = false;
|
|
17
|
+
function attach() {
|
|
18
|
+
if (attached || typeof document === 'undefined')
|
|
19
19
|
return;
|
|
20
|
-
|
|
20
|
+
attached = true;
|
|
21
21
|
document.addEventListener('pointerdown', (e) => { if (e.pointerType === 'touch')
|
|
22
|
-
|
|
23
|
-
document.addEventListener('touchstart', () => {
|
|
22
|
+
lastTouchAt = Date.now(); }, true);
|
|
23
|
+
document.addEventListener('touchstart', () => { lastTouchAt = Date.now(); }, { capture: true, passive: true });
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
attach();
|
|
26
26
|
/** 터치 포인터 이벤트인가 */
|
|
27
27
|
export function isTouchPointer(e) {
|
|
28
28
|
return !!e && e.pointerType === 'touch';
|
|
29
29
|
}
|
|
30
30
|
/** 방금(1초 안) 화면을 톡했나 — 톡 뒤 따라오는 focus·호환 마우스 이벤트를 거를 때 */
|
|
31
31
|
export function recentlyTouched(ms = 1000) {
|
|
32
|
-
|
|
33
|
-
return Date.now() -
|
|
32
|
+
attach();
|
|
33
|
+
return Date.now() - lastTouchAt < ms;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* 톡으로 뒤집는 손잡이. `open` 은 지금 열림, `setOpen(next, el)` 은 열기(el 은 앵커)·닫기.
|
|
@@ -45,39 +45,39 @@ export function useTouchToggle(open, setOpen, rootRef, { enabled = true, fallbac
|
|
|
45
45
|
const openRef = useLatestRef(open);
|
|
46
46
|
const setOpenRef = useLatestRef(setOpen);
|
|
47
47
|
/** 이 트리거 위 톡 누름 때의 열림(null 이면 톡 누름이 아니었다) */
|
|
48
|
-
const
|
|
49
|
-
const
|
|
48
|
+
const openOnPress = useRef(null);
|
|
49
|
+
const toggledAt = useRef(0);
|
|
50
50
|
useEffect(() => {
|
|
51
51
|
if (!enabled || typeof document === 'undefined')
|
|
52
52
|
return;
|
|
53
|
-
const
|
|
53
|
+
const onPress = (ev) => {
|
|
54
54
|
const pe = ev;
|
|
55
55
|
const t = ev.target;
|
|
56
56
|
const root = rootRef.current;
|
|
57
|
-
const
|
|
57
|
+
const inside = !!(t && root && root.contains(t));
|
|
58
58
|
if (pe.pointerType !== 'touch') {
|
|
59
|
-
if (
|
|
60
|
-
|
|
59
|
+
if (inside)
|
|
60
|
+
openOnPress.current = null;
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
|
-
if (
|
|
64
|
-
|
|
63
|
+
if (inside) {
|
|
64
|
+
openOnPress.current = openRef.current;
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
openOnPress.current = null;
|
|
68
68
|
if (openRef.current && !(t && t.closest?.('.tooltip-content')))
|
|
69
69
|
setOpenRef.current(false, root);
|
|
70
70
|
};
|
|
71
|
-
document.addEventListener('pointerdown',
|
|
72
|
-
return () => document.removeEventListener('pointerdown',
|
|
71
|
+
document.addEventListener('pointerdown', onPress, true);
|
|
72
|
+
return () => document.removeEventListener('pointerdown', onPress, true);
|
|
73
73
|
}, [enabled, rootRef, openRef, setOpenRef]);
|
|
74
74
|
const touchPress = useCallback((el) => {
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
75
|
+
const before = openOnPress.current;
|
|
76
|
+
if (before === null)
|
|
77
77
|
return false;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
setOpenRef.current(
|
|
78
|
+
openOnPress.current = null;
|
|
79
|
+
toggledAt.current = Date.now();
|
|
80
|
+
setOpenRef.current(!before, el ?? rootRef.current);
|
|
81
81
|
return true;
|
|
82
82
|
}, [rootRef, setOpenRef]);
|
|
83
83
|
/* 막힌 native 단추 — 포인터 이벤트가 안 오면 감싼 요소에서 톡(touchstart→touchend)을 받아 자리로 가린다 */
|
|
@@ -88,27 +88,27 @@ export function useTouchToggle(open, setOpen, rootRef, { enabled = true, fallbac
|
|
|
88
88
|
const parent = root?.parentElement;
|
|
89
89
|
if (!root || !parent)
|
|
90
90
|
return;
|
|
91
|
-
let
|
|
92
|
-
const
|
|
91
|
+
let openAtStart = null;
|
|
92
|
+
const isInside = (e) => {
|
|
93
93
|
const p = e.changedTouches?.[0];
|
|
94
94
|
if (!p)
|
|
95
95
|
return false;
|
|
96
96
|
const r = root.getBoundingClientRect();
|
|
97
97
|
return p.clientX >= r.left && p.clientX <= r.right && p.clientY >= r.top && p.clientY <= r.bottom;
|
|
98
98
|
};
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
if (
|
|
99
|
+
const onStart = (e) => { openAtStart = isInside(e) ? openRef.current : null; };
|
|
100
|
+
const onEnd = (e) => {
|
|
101
|
+
const before = openAtStart;
|
|
102
|
+
openAtStart = null;
|
|
103
|
+
if (before === null || Date.now() - toggledAt.current < 700 || !isInside(e))
|
|
104
104
|
return;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
setOpenRef.current(
|
|
105
|
+
openOnPress.current = null;
|
|
106
|
+
toggledAt.current = Date.now();
|
|
107
|
+
setOpenRef.current(!before, root);
|
|
108
108
|
};
|
|
109
|
-
parent.addEventListener('touchstart',
|
|
110
|
-
parent.addEventListener('touchend',
|
|
111
|
-
return () => { parent.removeEventListener('touchstart',
|
|
109
|
+
parent.addEventListener('touchstart', onStart, { passive: true });
|
|
110
|
+
parent.addEventListener('touchend', onEnd);
|
|
111
|
+
return () => { parent.removeEventListener('touchstart', onStart); parent.removeEventListener('touchend', onEnd); };
|
|
112
112
|
}, [enabled, fallbackOnParent, rootRef, openRef, setOpenRef]);
|
|
113
113
|
return { touchPress };
|
|
114
114
|
}
|
|
@@ -10,15 +10,15 @@ import { pushEsc } from './escStack';
|
|
|
10
10
|
* 트리거 조상 가운데 가장 높은 z-index 바로 위(없으면 `--z-menu`)로 둔다. 그 층들은 모두 알림·토스트 아래다.
|
|
11
11
|
*/
|
|
12
12
|
export function anchoredMenuZIndex(trigger) {
|
|
13
|
-
let
|
|
13
|
+
let tallest = 0;
|
|
14
14
|
if (trigger && typeof window !== 'undefined' && typeof window.getComputedStyle === 'function') {
|
|
15
15
|
for (let el = trigger; el && el !== document.body && el !== document.documentElement; el = el.parentElement) {
|
|
16
16
|
const z = parseInt(window.getComputedStyle(el).zIndex, 10);
|
|
17
|
-
if (!Number.isNaN(z) && z >
|
|
18
|
-
|
|
17
|
+
if (!Number.isNaN(z) && z > tallest)
|
|
18
|
+
tallest = z;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
return
|
|
21
|
+
return tallest > 0 ? `max(var(--z-menu, 1500), ${tallest + 1})` : 'var(--z-menu, 1500)';
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* 트리거 요소에 fixed 포지션으로 앵커링되는 메뉴/드롭다운/팝오버의 공용 로직.
|
|
@@ -53,28 +53,28 @@ export function useAnchoredMenu({ computeStyle, onScroll = 'close', onResize = '
|
|
|
53
53
|
(약 40px)이 오르내린다. 높이가 바뀔 때마다 다시 센다.
|
|
54
54
|
**높이가 달라졌을 때만** 다시 센다 — 안 그러면 셈이 자리를 바꾸고 자리가 다시
|
|
55
55
|
셈을 불러 끝없이 돈다. */
|
|
56
|
-
let
|
|
57
|
-
if (
|
|
56
|
+
let measuredHeight = menuRef.current?.offsetHeight ?? 0;
|
|
57
|
+
if (measuredHeight > 0)
|
|
58
58
|
recalc();
|
|
59
59
|
/* **오가는 것을 끊는다.** 높이가 달라질 때만 세는 것으로는 두 꼴을 번갈아 오가는 것을
|
|
60
60
|
못 막는다(A 로 자리를 잡으면 높이가 B 가 되고, B 로 잡으면 다시 A 가 되는 꼴).
|
|
61
61
|
한 번 여는 동안 몇 번까지만 센다 — 제대로 된 다시 재기는 한두 번이면 끝난다. */
|
|
62
|
-
let
|
|
63
|
-
let
|
|
62
|
+
let measureCount = 0;
|
|
63
|
+
let panelObserver = null;
|
|
64
64
|
if (typeof ResizeObserver !== 'undefined' && menuRef.current) {
|
|
65
|
-
|
|
65
|
+
panelObserver = new ResizeObserver(() => {
|
|
66
66
|
const h = menuRef.current?.offsetHeight ?? 0;
|
|
67
|
-
if (h ===
|
|
67
|
+
if (h === measuredHeight)
|
|
68
68
|
return;
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
measuredHeight = h;
|
|
70
|
+
if (++measureCount > 4) {
|
|
71
|
+
panelObserver?.disconnect();
|
|
72
|
+
panelObserver = null;
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
recalc();
|
|
76
76
|
});
|
|
77
|
-
|
|
77
|
+
panelObserver.observe(menuRef.current);
|
|
78
78
|
}
|
|
79
79
|
const handleOutsideClick = (e) => {
|
|
80
80
|
const target = e.target;
|
|
@@ -94,21 +94,21 @@ export function useAnchoredMenu({ computeStyle, onScroll = 'close', onResize = '
|
|
|
94
94
|
capture 로** 받는다 — `DatePicker` 가 `onScroll: 'reposition'` 을 주므로 달력이 열린 동안 굴리면
|
|
95
95
|
틀마다 여러 번 돌았다. **닫는 길('close')은 안 미룬다** — 미루면 앵커에서 떨어진 판이 한 틀 더
|
|
96
96
|
허공에 남는다(Tooltip 과 같은 가름). 미루는 것은 *자리* 뿐이다. */
|
|
97
|
-
let
|
|
98
|
-
const
|
|
97
|
+
let rafId = null;
|
|
98
|
+
const reposition = () => {
|
|
99
99
|
if (typeof requestAnimationFrame === 'undefined') {
|
|
100
100
|
recalc();
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
|
-
if (
|
|
103
|
+
if (rafId !== null)
|
|
104
104
|
return;
|
|
105
|
-
|
|
105
|
+
rafId = requestAnimationFrame(() => { rafId = null; recalc(); });
|
|
106
106
|
};
|
|
107
107
|
const handleScroll = (e) => {
|
|
108
108
|
if (menuRef.current?.contains(e.target))
|
|
109
109
|
return;
|
|
110
110
|
if (onScroll === 'reposition')
|
|
111
|
-
|
|
111
|
+
reposition();
|
|
112
112
|
else if (onScroll === 'close')
|
|
113
113
|
setOpen(false);
|
|
114
114
|
};
|
|
@@ -116,17 +116,17 @@ export function useAnchoredMenu({ computeStyle, onScroll = 'close', onResize = '
|
|
|
116
116
|
window.addEventListener('scroll', handleScroll, true);
|
|
117
117
|
const handleResize = () => {
|
|
118
118
|
if (onResize === 'reposition')
|
|
119
|
-
|
|
119
|
+
reposition();
|
|
120
120
|
else if (onResize === 'close')
|
|
121
121
|
setOpen(false);
|
|
122
122
|
};
|
|
123
123
|
/* 회전(orientationchange)도 함께 — iOS Safari 는 회전 직후 resize 가 늦거나 옛 크기로 오는 일이 있어 회전 뒤 한 번 더 잰다 */
|
|
124
|
-
let
|
|
124
|
+
let rotateTimer = null;
|
|
125
125
|
const handleOrientation = () => {
|
|
126
126
|
handleResize();
|
|
127
|
-
if (
|
|
128
|
-
clearTimeout(
|
|
129
|
-
|
|
127
|
+
if (rotateTimer)
|
|
128
|
+
clearTimeout(rotateTimer);
|
|
129
|
+
rotateTimer = setTimeout(() => { rotateTimer = null; if (onResize === 'reposition')
|
|
130
130
|
recalc(); }, 250);
|
|
131
131
|
};
|
|
132
132
|
if (onResize !== 'ignore') {
|
|
@@ -134,9 +134,9 @@ export function useAnchoredMenu({ computeStyle, onScroll = 'close', onResize = '
|
|
|
134
134
|
window.addEventListener('orientationchange', handleOrientation);
|
|
135
135
|
}
|
|
136
136
|
return () => {
|
|
137
|
-
if (
|
|
138
|
-
cancelAnimationFrame(
|
|
139
|
-
|
|
137
|
+
if (rafId !== null)
|
|
138
|
+
cancelAnimationFrame(rafId);
|
|
139
|
+
panelObserver?.disconnect();
|
|
140
140
|
document.removeEventListener('mousedown', handleOutsideClick);
|
|
141
141
|
document.removeEventListener('pointerdown', handleOutsideClick);
|
|
142
142
|
if (onScroll !== 'ignore')
|
|
@@ -145,8 +145,8 @@ export function useAnchoredMenu({ computeStyle, onScroll = 'close', onResize = '
|
|
|
145
145
|
window.removeEventListener('resize', handleResize);
|
|
146
146
|
window.removeEventListener('orientationchange', handleOrientation);
|
|
147
147
|
}
|
|
148
|
-
if (
|
|
149
|
-
clearTimeout(
|
|
148
|
+
if (rotateTimer)
|
|
149
|
+
clearTimeout(rotateTimer);
|
|
150
150
|
};
|
|
151
151
|
}, [open, recalc, onScroll, onResize, ignoreOutsideClickSelector]);
|
|
152
152
|
/* ESC 로 닫기 — **공유 ESC 스택**에 올린다(Popup 과 같은 스택). 전에는 document 에서 따로 들어,
|
|
@@ -34,25 +34,25 @@ export function useDailyTrainingSelection(userId) {
|
|
|
34
34
|
const [hasData, setHasData] = useState(false);
|
|
35
35
|
/* 오늘(KST) — 자정을 넘기면 바뀌어 아래 선택을 **새 날짜 시드로** 다시 계산하게 한다. 전에는 userId 만 봐서 화면을 열어 둔 채
|
|
36
36
|
자정을 넘기면 어제 시드의 세트가 '오늘' 로 보이는데 완료 기록에는 새 날짜가 찍혔다. */
|
|
37
|
-
const [
|
|
37
|
+
const [today, setToday] = useState(() => getTodayKst());
|
|
38
38
|
useEffect(() => {
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const kst = new Date(
|
|
42
|
-
const
|
|
43
|
-
return Math.max(1000,
|
|
39
|
+
const msUntilMidnight = () => {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const kst = new Date(now + 9 * 3600_000);
|
|
42
|
+
const midnight = Date.UTC(kst.getUTCFullYear(), kst.getUTCMonth(), kst.getUTCDate() + 1) - 9 * 3600_000;
|
|
43
|
+
return Math.max(1000, midnight - now + 1000);
|
|
44
44
|
};
|
|
45
|
-
let t = setTimeout(function
|
|
45
|
+
let t = setTimeout(function tick() { setToday(getTodayKst()); t = setTimeout(tick, msUntilMidnight()); }, msUntilMidnight());
|
|
46
46
|
return () => clearTimeout(t);
|
|
47
47
|
}, []);
|
|
48
48
|
useEffect(() => {
|
|
49
49
|
let cancelled = false;
|
|
50
|
-
let
|
|
51
|
-
let
|
|
50
|
+
let left = false;
|
|
51
|
+
let applied = false;
|
|
52
52
|
let timer;
|
|
53
53
|
let attempts = 0;
|
|
54
|
-
let
|
|
55
|
-
let
|
|
54
|
+
let fetching = null;
|
|
55
|
+
let nextFetchAt = 0; // 이 attempts 에 이르면 자료 받기를 다시 청한다(2초 = 8틱마다)
|
|
56
56
|
const MAX_ATTEMPTS = 40; // 250ms * 40 = 10초 후 폴링 중단(풀이 영영 비는 경우 무한루프 방지)
|
|
57
57
|
const stop = () => { if (timer !== undefined) {
|
|
58
58
|
clearInterval(timer);
|
|
@@ -66,9 +66,9 @@ export function useDailyTrainingSelection(userId) {
|
|
|
66
66
|
}
|
|
67
67
|
catch { }
|
|
68
68
|
const apply = (sel) => {
|
|
69
|
-
if (
|
|
69
|
+
if (left)
|
|
70
70
|
return;
|
|
71
|
-
|
|
71
|
+
applied = true;
|
|
72
72
|
todayRef.current = sel.today;
|
|
73
73
|
yesterdayRef.current = sel.yesterday;
|
|
74
74
|
setLoaded(true);
|
|
@@ -92,19 +92,19 @@ export function useDailyTrainingSelection(userId) {
|
|
|
92
92
|
배포(형식 오류)에서 10초에 850KB 자료를 40번 받았다. 그 사이 폴링은 선택만 다시 계산한다. */
|
|
93
93
|
/* **받는 중이면 이 틱은 건너뛴다** — 받기는 3초까지 기다리는데 250ms 틱이 그 사이 attempts 가 안 오른 채 들어와
|
|
94
94
|
ensureTrainingDataLoaded 를 매번 새로 불러, 끝나지 않은 받기마다 3초 타이머가 열두 개씩 쌓였다. 진행 중인 약속을 하나만 둔다. */
|
|
95
|
-
if (
|
|
95
|
+
if (fetching) {
|
|
96
96
|
if (++attempts >= MAX_ATTEMPTS)
|
|
97
|
-
|
|
97
|
+
giveUp();
|
|
98
98
|
return;
|
|
99
99
|
} // 건너뛴 틱도 10초 시한에 센다
|
|
100
|
-
if (attempts >=
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
if (attempts >= nextFetchAt) {
|
|
101
|
+
nextFetchAt = attempts + 8;
|
|
102
|
+
fetching = ensureTrainingDataLoaded(3000).catch(() => false);
|
|
103
103
|
try {
|
|
104
|
-
await
|
|
104
|
+
await fetching;
|
|
105
105
|
}
|
|
106
106
|
finally {
|
|
107
|
-
|
|
107
|
+
fetching = null;
|
|
108
108
|
}
|
|
109
109
|
if (cancelled)
|
|
110
110
|
return;
|
|
@@ -118,35 +118,35 @@ export function useDailyTrainingSelection(userId) {
|
|
|
118
118
|
// 포기한다. hasData 를 반드시 false 로 되돌린다 — userId 가 바뀌어 다시 돌 때,
|
|
119
119
|
// 앞 사람 것이 성공했다면 true 로 남아 있어 "자료가 있다" 고 거짓말하게 된다.
|
|
120
120
|
if (++attempts >= MAX_ATTEMPTS)
|
|
121
|
-
|
|
121
|
+
giveUp();
|
|
122
122
|
};
|
|
123
|
-
function
|
|
123
|
+
function giveUp() { setLoaded(true); setHasData(false); cancelled = true; stop(); }
|
|
124
124
|
/* 포기한 뒤(10초)라도 자료가 도착하면 다시 계산한다 — 느린 회선에서 13초 걸려 받은 회원은 새로고침 전까지 "자료를 못 찾음" 에 남았다 */
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
125
|
+
const onData = () => {
|
|
126
|
+
if (left || applied)
|
|
127
127
|
return;
|
|
128
128
|
const sel = resolveSelection(forceRefresh, undefined, userId);
|
|
129
129
|
if (hasItems(sel.today))
|
|
130
130
|
apply(sel);
|
|
131
131
|
};
|
|
132
132
|
try {
|
|
133
|
-
window.addEventListener('speechear.data.updated',
|
|
133
|
+
window.addEventListener('speechear.data.updated', onData);
|
|
134
134
|
}
|
|
135
135
|
catch { }
|
|
136
136
|
tryFill();
|
|
137
137
|
timer = setInterval(tryFill, 250);
|
|
138
138
|
return () => {
|
|
139
139
|
cancelled = true;
|
|
140
|
-
|
|
140
|
+
left = true;
|
|
141
141
|
stop();
|
|
142
142
|
try {
|
|
143
|
-
window.removeEventListener('speechear.data.updated',
|
|
143
|
+
window.removeEventListener('speechear.data.updated', onData);
|
|
144
144
|
}
|
|
145
145
|
catch { }
|
|
146
146
|
};
|
|
147
147
|
// 오늘 은 날짜가 바뀌었다는 신호 — resolveSelection 이 안에서 오늘 날짜를 읽는다
|
|
148
148
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
149
|
-
}, [userId,
|
|
149
|
+
}, [userId, today]);
|
|
150
150
|
const getTrainingPerType = useCallback((typeKey) => {
|
|
151
151
|
const src = showYesterday ? yesterdayRef.current : todayRef.current;
|
|
152
152
|
const fallback = showYesterday ? todayRef.current : yesterdayRef.current;
|
|
@@ -26,8 +26,8 @@ export const DocumentTitleSuppressContext = createContext(false);
|
|
|
26
26
|
/* 제목을 건 화면들 — 나중에 건 것이 위. 떠날 때 **남은 맨 위 제목**으로 돌린다.
|
|
27
27
|
전에는 저마다 건 순간의 제목(prev)을 기억했다가 되돌려, A 가 떠 있는 동안 B 가 붙고 A 가 나중에 떠나면
|
|
28
28
|
A 의 prev 로 돌아가 B 의 제목이 지워졌다(말귀 CustomersPage 가 겪고 우회해 둠). */
|
|
29
|
-
const
|
|
30
|
-
let
|
|
29
|
+
const titleStack = [];
|
|
30
|
+
let originalTitle = null;
|
|
31
31
|
/**
|
|
32
32
|
* 현재 화면의 제목을 `document.title`에 반영한다 — `'<제목> · <앱 이름>'`.
|
|
33
33
|
*
|
|
@@ -47,16 +47,16 @@ export function useDocumentTitle(title) {
|
|
|
47
47
|
return;
|
|
48
48
|
if (typeof title !== 'string' || !title)
|
|
49
49
|
return;
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
document.title =
|
|
50
|
+
if (titleStack.length === 0)
|
|
51
|
+
originalTitle = document.title;
|
|
52
|
+
const entry = { text: BASE_TITLE ? `${title}${SEP}${BASE_TITLE}` : title };
|
|
53
|
+
titleStack.push(entry);
|
|
54
|
+
document.title = entry.text;
|
|
55
55
|
return () => {
|
|
56
|
-
const i =
|
|
56
|
+
const i = titleStack.lastIndexOf(entry);
|
|
57
57
|
if (i >= 0)
|
|
58
|
-
|
|
59
|
-
document.title =
|
|
58
|
+
titleStack.splice(i, 1);
|
|
59
|
+
document.title = titleStack.length ? titleStack[titleStack.length - 1].text : (originalTitle ?? document.title);
|
|
60
60
|
};
|
|
61
61
|
}, [title, suppressed]);
|
|
62
62
|
}
|
|
@@ -43,13 +43,13 @@ export function useEditableList(emptyItem, initialList = []) {
|
|
|
43
43
|
/* 편집이 **취소로** 끝나면 그 사이 건너뛴 목록을 받는다 — 전에는 initialList 가 바뀔 때만 돌아, 편집 중 들어온 서버 목록이
|
|
44
44
|
취소한 뒤에도 끝내 안 보였다(방문 예정을 고치다 취소하면 그 사이 다른 방문이 빠진 채). commitEdit 으로 끝난 때는
|
|
45
45
|
방금 커밋한 편집을 옛 목록으로 덮지 않도록 한 번 넘긴다(다음 목록이 오면 그때 받는다). */
|
|
46
|
-
const
|
|
46
|
+
const endedByCommitRef = useRef(false);
|
|
47
47
|
const prevInitialRef = useRef(null);
|
|
48
48
|
useEffect(() => {
|
|
49
49
|
if (isEditing)
|
|
50
50
|
return;
|
|
51
|
-
if (
|
|
52
|
-
|
|
51
|
+
if (endedByCommitRef.current) {
|
|
52
|
+
endedByCommitRef.current = false;
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
const prev = prevInitialRef.current;
|
|
@@ -80,7 +80,7 @@ export function useEditableList(emptyItem, initialList = []) {
|
|
|
80
80
|
setSelected(updated);
|
|
81
81
|
// 편집 중이었을 때만 표시한다(편집이 아닌데 표시하면 다음 목록 한 번을 헛되이 건너뛴다). 같은 값을 넣는 표시라 두 번 불려도 같다
|
|
82
82
|
setIsEditing(prev => { if (prev)
|
|
83
|
-
|
|
83
|
+
endedByCommitRef.current = true; return false; });
|
|
84
84
|
}, []);
|
|
85
85
|
const remove = useCallback((item) => {
|
|
86
86
|
setList(prev => prev.filter(v => !isSameEditableItem(v, item)));
|
|
@@ -40,8 +40,8 @@ export function useFocusTrap(panelRef, enabled = true) {
|
|
|
40
40
|
const active = document.activeElement;
|
|
41
41
|
/* 포커스가 **판 자체**(tabIndex=-1 — 본문 글자를 누르면 판이 받는다)에 있으면 첫 칸·끝 칸 어느 쪽도 아니라
|
|
42
42
|
되감지 않아, Shift+Tab 한 번에 뒤 화면으로 나갔다. 판 자체면 Shift+Tab 은 끝 칸으로 보낸다. */
|
|
43
|
-
const
|
|
44
|
-
if (e.shiftKey && (active === first ||
|
|
43
|
+
const onPanelItself = active === panel;
|
|
44
|
+
if (e.shiftKey && (active === first || onPanelItself)) {
|
|
45
45
|
e.preventDefault();
|
|
46
46
|
last.focus();
|
|
47
47
|
}
|
|
@@ -5,7 +5,7 @@ export default function useFontScale({ storageKey = 'fontScale',
|
|
|
5
5
|
// 기본값을 중앙화: Training/Tooltip 텍스트 박스 공통값
|
|
6
6
|
baseFont = 20, minScale = 0.8, maxScale = 1.4, step = 0.1, lineHeightRatio = 1.4, } = {}) {
|
|
7
7
|
const clamp = (v) => Math.max(minScale, Math.min(maxScale, Number(v)));
|
|
8
|
-
const
|
|
8
|
+
const read = (key) => {
|
|
9
9
|
try {
|
|
10
10
|
const raw = localStorage.getItem(key);
|
|
11
11
|
const parsed = raw ? Number(raw) : 1;
|
|
@@ -21,13 +21,13 @@ baseFont = 20, minScale = 0.8, maxScale = 1.4, step = 0.1, lineHeightRatio = 1.4
|
|
|
21
21
|
return clamp(1);
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
|
-
const [scale, setScaleRaw] = useState(() =>
|
|
24
|
+
const [scale, setScaleRaw] = useState(() => read(storageKey));
|
|
25
25
|
/* 열쇠가 바뀌면 **그 열쇠의 저장값** 으로 — 전에는 앞 열쇠의 값을 들고 있다가 아래 쓰기가 새 열쇠의 저장값을 덮었다.
|
|
26
26
|
렌더 중에 맞추므로 쓰기 effect 는 새 값으로만 돈다. */
|
|
27
|
-
const [
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
setScaleRaw(
|
|
27
|
+
const [loadedKey, setLoadedKey] = useState(storageKey);
|
|
28
|
+
if (loadedKey !== storageKey) {
|
|
29
|
+
setLoadedKey(storageKey);
|
|
30
|
+
setScaleRaw(read(storageKey));
|
|
31
31
|
}
|
|
32
32
|
useEffect(() => {
|
|
33
33
|
try {
|