@ihabdevteam/core 0.321.0 → 0.323.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +617 -0
- package/dist/components.cjs.js +183 -183
- package/dist/components.esm.css +1 -1
- package/dist/hooks.cjs.js +3 -3
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core.cjs.js +187 -187
- package/dist/ihabdevteam-core.d.ts +108 -66
- package/dist/packages/core/src/components/Accordion.js +3 -3
- package/dist/packages/core/src/components/Alert.js +2 -2
- package/dist/packages/core/src/components/BarChart.js +120 -119
- package/dist/packages/core/src/components/BottomNav.js +18 -18
- package/dist/packages/core/src/components/BulkBar.js +25 -25
- package/dist/packages/core/src/components/Button.js +16 -16
- package/dist/packages/core/src/components/Calendar.js +17 -17
- package/dist/packages/core/src/components/ChartSection.js +30 -30
- package/dist/packages/core/src/components/CountTile.js +38 -38
- package/dist/packages/core/src/components/DatePicker.js +59 -59
- package/dist/packages/core/src/components/Dialog.js +14 -14
- package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.js +8 -8
- package/dist/packages/core/src/components/FilterPanel.js +6 -6
- package/dist/packages/core/src/components/FilterSectionInfo.js +6 -6
- package/dist/packages/core/src/components/GameIntro.js +1 -1
- package/dist/packages/core/src/components/HearingDeviceInfo.d.ts +5 -5
- package/dist/packages/core/src/components/HearingDeviceInfo.js +2 -2
- package/dist/packages/core/src/components/Icon.js +2 -2
- package/dist/packages/core/src/components/ItemActions.js +16 -16
- package/dist/packages/core/src/components/LandingHeader.js +6 -6
- package/dist/packages/core/src/components/LineChart.js +24 -24
- package/dist/packages/core/src/components/Loader.js +5 -5
- package/dist/packages/core/src/components/NoticeBox.js +3 -3
- package/dist/packages/core/src/components/PageTitle.js +2 -2
- package/dist/packages/core/src/components/Pagination.js +4 -4
- package/dist/packages/core/src/components/Panel.js +2 -2
- package/dist/packages/core/src/components/PanelRow.d.ts +2 -4
- package/dist/packages/core/src/components/PanelRow.js +8 -15
- package/dist/packages/core/src/components/PassCard/PassCard.js +9 -9
- package/dist/packages/core/src/components/PassCard/passCardProduct.js +2 -2
- package/dist/packages/core/src/components/PaymentPlan.js +5 -5
- package/dist/packages/core/src/components/Popup/ConfirmPopup.js +29 -29
- package/dist/packages/core/src/components/Popup/CouponInputPopup.js +2 -2
- package/dist/packages/core/src/components/Popup/DeviceListPopup.d.ts +4 -8
- package/dist/packages/core/src/components/Popup/DeviceListPopup.js +1 -1
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +19 -26
- package/dist/packages/core/src/components/Popup/ListPopup.js +6 -6
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +10 -10
- package/dist/packages/core/src/components/Popup/Popup.js +27 -27
- package/dist/packages/core/src/components/Popup/SaveTablePopup.js +5 -5
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +4 -4
- package/dist/packages/core/src/components/Popup/customerDetailUtils.js +18 -18
- package/dist/packages/core/src/components/Popup/useDiscardGuard.js +4 -4
- package/dist/packages/core/src/components/ProgressIndicator.js +5 -5
- package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +16 -16
- package/dist/packages/core/src/components/Rating.js +20 -20
- package/dist/packages/core/src/components/Report/ReportDocument.js +21 -21
- package/dist/packages/core/src/components/RingChart.js +5 -5
- package/dist/packages/core/src/components/SectionHeadFloat.js +15 -15
- package/dist/packages/core/src/components/SegmentedControl.js +34 -34
- package/dist/packages/core/src/components/SideNav.js +37 -37
- package/dist/packages/core/src/components/Slider.js +2 -2
- package/dist/packages/core/src/components/SoundCompass.js +5 -5
- package/dist/packages/core/src/components/Table.js +257 -98
- package/dist/packages/core/src/components/Tabs.js +12 -12
- package/dist/packages/core/src/components/TextField.js +34 -34
- package/dist/packages/core/src/components/Toast.js +6 -6
- package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +4 -4
- package/dist/packages/core/src/components/Tooltip/Tooltip.js +22 -22
- package/dist/packages/core/src/components/Tooltip/TrainingTooltip.js +4 -4
- package/dist/packages/core/src/components/TopBanner.js +28 -28
- package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +11 -11
- package/dist/packages/core/src/components/Training/CategoryIcon.js +2 -2
- package/dist/packages/core/src/components/Training/NoisePlayerContext.d.ts +7 -4
- package/dist/packages/core/src/components/Training/NoisePlayerContext.js +12 -12
- package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingBlock.js +12 -12
- package/dist/packages/core/src/components/Training/TrainingBlockButton.js +10 -10
- package/dist/packages/core/src/components/Training/TrainingBlockGrid.js +21 -21
- package/dist/packages/core/src/components/Training/TrainingCard.css +13 -1
- package/dist/packages/core/src/components/Training/TrainingCard.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingCardGroup.js +6 -6
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +83 -83
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +17 -17
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +128 -128
- package/dist/packages/core/src/components/Training/TrainingStepCard.js +18 -18
- package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +4 -4
- package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +12 -12
- package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +13 -13
- package/dist/packages/core/src/components/Training/TrainingStepTitle.js +4 -4
- package/dist/packages/core/src/components/Training/TrainingWordCard.js +17 -17
- package/dist/packages/core/src/components/Training/WordTablePopup.js +5 -5
- package/dist/packages/core/src/components/Training/audioPlaybackRules.js +8 -8
- package/dist/packages/core/src/components/Training/audioTimeline.js +25 -25
- package/dist/packages/core/src/components/Training/oneShotSound.d.ts +1 -1
- package/dist/packages/core/src/components/Training/oneShotSound.js +14 -14
- package/dist/packages/core/src/components/Training/stepAnswerable.d.ts +2 -2
- package/dist/packages/core/src/components/Training/trainingMissStats.js +22 -22
- package/dist/packages/core/src/components/Training/trainingPopupUtils.js +7 -7
- package/dist/packages/core/src/components/Training/useAudioPreview.js +15 -15
- package/dist/packages/core/src/components/Training/useFloatingFocus.js +14 -14
- package/dist/packages/core/src/components/Training/useNoiseFade.js +22 -22
- package/dist/packages/core/src/components/Training/useObjectUrls.js +2 -2
- package/dist/packages/core/src/components/Training/usePlaybackSequencer.d.ts +2 -2
- package/dist/packages/core/src/components/Training/usePlaybackSequencer.js +23 -23
- package/dist/packages/core/src/components/Training/useProgressTicker.js +33 -33
- package/dist/packages/core/src/components/Training/useTTSPlayback.js +17 -17
- package/dist/packages/core/src/components/Training/useWaveReady.js +11 -11
- package/dist/packages/core/src/components/UpdateAlert.js +6 -6
- package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +7 -1
- package/dist/packages/core/src/components/Word/TableFilterPanel.js +3 -3
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +18 -18
- package/dist/packages/core/src/components/Word/WordTableBlock.js +28 -28
- package/dist/packages/core/src/components/Word/WordTableButton.css +16 -2
- package/dist/packages/core/src/components/Word/WordTableThumbnail.css +6 -1
- package/dist/packages/core/src/components/Word/generatedDraft.js +14 -14
- package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -7
- package/dist/packages/core/src/components/Word/wordScoreStats.js +17 -17
- package/dist/packages/core/src/components/Word/wordTablePrint.js +10 -10
- package/dist/packages/core/src/components/Word/wordTableRecommend.js +13 -13
- package/dist/packages/core/src/components/Word/wordTableReview.js +28 -28
- package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +8 -11
- package/dist/packages/core/src/components/Word/wordTableSettings.js +48 -36
- package/dist/packages/core/src/components/WordTable.js +130 -130
- package/dist/packages/core/src/components/barChartData.d.ts +16 -1
- package/dist/packages/core/src/components/barChartData.js +49 -29
- package/dist/packages/core/src/components/barChartTypes.js +8 -8
- package/dist/packages/core/src/components/tableTrackList.js +11 -11
- package/dist/packages/core/src/components/updateAlertStack.js +28 -28
- package/dist/packages/core/src/components/useBarChartControl.js +4 -6
- package/dist/packages/core/src/components/useComboboxSuggestions.js +17 -17
- package/dist/packages/core/src/components/useToastLifecycle.js +18 -18
- package/dist/packages/core/src/components/wordTableLayout.d.ts +22 -7
- package/dist/packages/core/src/components/wordTableLayout.js +5 -5
- package/dist/packages/core/src/context/I18nProvider.js +6 -6
- package/dist/packages/core/src/hooks/focusReturn.js +27 -27
- package/dist/packages/core/src/hooks/inputModality.d.ts +1 -1
- package/dist/packages/core/src/hooks/inputModality.js +14 -14
- package/dist/packages/core/src/hooks/touchToggle.js +39 -39
- package/dist/packages/core/src/hooks/useAnchoredMenu.js +30 -30
- package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +28 -28
- package/dist/packages/core/src/hooks/useDeviceManagement.d.ts +5 -17
- package/dist/packages/core/src/hooks/useDeviceManagement.js +20 -8
- package/dist/packages/core/src/hooks/useDocumentTitle.js +10 -10
- package/dist/packages/core/src/hooks/useEditableList.js +4 -4
- package/dist/packages/core/src/hooks/useFocusTrap.js +2 -2
- package/dist/packages/core/src/hooks/useFontScale.js +6 -6
- package/dist/packages/core/src/hooks/useIcon.js +7 -7
- package/dist/packages/core/src/hooks/useListControls.js +2 -2
- package/dist/packages/core/src/hooks/usePageResetOnChange.d.ts +3 -3
- package/dist/packages/core/src/hooks/usePageResetOnChange.js +6 -6
- package/dist/packages/core/src/hooks/usePasswordChange.js +4 -4
- package/dist/packages/core/src/hooks/usePendingPress.js +7 -7
- package/dist/packages/core/src/hooks/useRelativeDay.js +4 -4
- package/dist/packages/core/src/hooks/useSoundCompassExam.js +6 -6
- package/dist/packages/core/src/hooks/useTablePaging.d.ts +7 -7
- package/dist/packages/core/src/hooks/useTablePaging.js +40 -34
- package/dist/packages/core/src/hooks/useTrainingSessionLog.js +35 -35
- package/dist/packages/core/src/hooks/useVisitSchedule.js +4 -4
- package/dist/packages/core/src/hooks/useWordAttributeFilter.js +14 -14
- package/dist/packages/core/src/hooks/useWordMeanings.js +5 -5
- package/dist/packages/core/src/hooks/useWordTrainingFilter.d.ts +3 -1
- package/dist/packages/core/src/hooks/useWordTrainingFilter.js +14 -12
- package/dist/packages/core/src/i18n/locales/en.json +13 -2
- package/dist/packages/core/src/i18n/locales/ko.json +12 -1
- package/dist/packages/core/src/types/wordSearch.d.ts +9 -0
- package/dist/packages/core/src/utils/audioMeter.js +5 -5
- package/dist/packages/core/src/utils/audioPeaks.d.ts +1 -1
- package/dist/packages/core/src/utils/audioPeaks.js +18 -18
- package/dist/packages/core/src/utils/blockGrid.js +4 -4
- package/dist/packages/core/src/utils/chunkSkew.js +9 -9
- package/dist/packages/core/src/utils/clipboard.js +3 -3
- package/dist/packages/core/src/utils/dailySelectionUtils.js +12 -12
- package/dist/packages/core/src/utils/date.js +12 -12
- package/dist/packages/core/src/utils/devWarn.d.ts +2 -2
- package/dist/packages/core/src/utils/devWarn.js +7 -7
- package/dist/packages/core/src/utils/deviceKind.d.ts +37 -7
- package/dist/packages/core/src/utils/deviceKind.js +53 -10
- package/dist/packages/core/src/utils/errorMessage.js +7 -7
- package/dist/packages/core/src/utils/filterPanelSummary.d.ts +1 -1
- package/dist/packages/core/src/utils/filterPanelSummary.js +16 -16
- package/dist/packages/core/src/utils/format.js +4 -4
- package/dist/packages/core/src/utils/getTrainingData.js +9 -9
- package/dist/packages/core/src/utils/imageConfig.js +17 -17
- package/dist/packages/core/src/utils/koreanSyllable.js +18 -18
- package/dist/packages/core/src/utils/navScroll.d.ts +2 -2
- package/dist/packages/core/src/utils/navScroll.js +6 -6
- package/dist/packages/core/src/utils/phonemeConvert.d.ts +7 -0
- package/dist/packages/core/src/utils/phonemeConvert.js +19 -10
- package/dist/packages/core/src/utils/phonemeGroups.js +2 -2
- package/dist/packages/core/src/utils/phonemePairSearch.d.ts +1 -0
- package/dist/packages/core/src/utils/phonemePairSearch.js +118 -102
- package/dist/packages/core/src/utils/phonemeSectionCodes.d.ts +52 -0
- package/dist/packages/core/src/utils/phonemeSectionCodes.js +76 -0
- package/dist/packages/core/src/utils/prefetchChunk.js +6 -6
- package/dist/packages/core/src/utils/pressable.d.ts +1 -1
- package/dist/packages/core/src/utils/pressable.js +3 -3
- package/dist/packages/core/src/utils/safeHref.js +4 -4
- package/dist/packages/core/src/utils/savedWordTables.js +5 -5
- package/dist/packages/core/src/utils/sessionTab.d.ts +7 -7
- package/dist/packages/core/src/utils/sessionTab.js +9 -9
- package/dist/packages/core/src/utils/speak.js +12 -12
- package/dist/packages/core/src/utils/textSort.d.ts +5 -5
- package/dist/packages/core/src/utils/textSort.js +47 -47
- package/dist/packages/core/src/utils/theme.js +3 -3
- package/dist/packages/core/src/utils/trainingItemResults.js +3 -3
- package/dist/packages/core/src/utils/ttsConfig.js +2 -2
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +2 -2
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +48 -48
- package/dist/packages/core/src/utils/wordContrast.js +3 -3
- package/dist/packages/core/src/utils/wordPairIndex.js +4 -4
- package/dist/packages/core/src/utils/wordTrainingGenerators.js +4 -4
- package/dist/packages/core/src/utils/wordTrainingPool.js +15 -15
- package/dist/packages/core/src/utils/wordTrainingRuntime.js +6 -6
- package/dist/packages/core/src/utils/wornDate.d.ts +3 -3
- package/dist/packages/core/src/utils/wornDate.js +14 -14
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +4 -4
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** 마지막 입력이 키보드였으면 true(모르면 false) */
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function wasKeyboardPress(): boolean;
|
|
@@ -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
|
-
export function
|
|
29
|
-
|
|
30
|
-
return
|
|
28
|
+
export function wasKeyboardPress() {
|
|
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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DeviceForm } from '../types/device';
|
|
2
|
-
type Side = '
|
|
2
|
+
type Side = 'left' | 'right';
|
|
3
3
|
/**
|
|
4
4
|
* 보청기 기기 등록 / 이력 관리 상태를 캡슐화한 훅.
|
|
5
5
|
*/
|
|
@@ -11,22 +11,10 @@ export declare function useDeviceManagement(): {
|
|
|
11
11
|
setShowDevice: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
12
12
|
deviceDirection: Side;
|
|
13
13
|
setDeviceDirection: import("react").Dispatch<import("react").SetStateAction<Side>>;
|
|
14
|
-
devices:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
setDevices: import("react").Dispatch<import("react").SetStateAction<{
|
|
19
|
-
좌: DeviceForm | null;
|
|
20
|
-
우: DeviceForm | null;
|
|
21
|
-
}>>;
|
|
22
|
-
deviceHistory: {
|
|
23
|
-
좌: DeviceForm[];
|
|
24
|
-
우: DeviceForm[];
|
|
25
|
-
};
|
|
26
|
-
setDeviceHistory: import("react").Dispatch<import("react").SetStateAction<{
|
|
27
|
-
좌: DeviceForm[];
|
|
28
|
-
우: DeviceForm[];
|
|
29
|
-
}>>;
|
|
14
|
+
devices: Record<Side, DeviceForm | null>;
|
|
15
|
+
setDevices: import("react").Dispatch<import("react").SetStateAction<Record<Side, DeviceForm | null>>>;
|
|
16
|
+
deviceHistory: Record<Side, DeviceForm[]>;
|
|
17
|
+
setDeviceHistory: import("react").Dispatch<import("react").SetStateAction<Record<Side, DeviceForm[]>>>;
|
|
30
18
|
showDeviceList: boolean;
|
|
31
19
|
setShowDeviceList: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
32
20
|
listForSide: Side;
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useCallback, useState } from 'react';
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* `form.direction` 이 `'left'`·`'right'` 가 아니면 null — 캐스팅만 하면 `devices['엉뚱한값']` 같은 열쇠가 생긴다.
|
|
5
|
+
*
|
|
6
|
+
* **옛 값(`'좌'`·`'우'`)은 받지 않는다.** 2026-10-13 사장님 결정으로 값을 영어로 옮겼고,
|
|
7
|
+
* 너그럽게 받는 몫은 **소비자가 진다** — 말귀가 DB 와 오가는 길목 한 자리에 세워 두었다
|
|
8
|
+
* (`toDbDirection`/`fromDbDirection`). 여기서 함께 받으면 **두 자리가 같은 일을 하고**,
|
|
9
|
+
* DB 가 다 옮겨진 뒤에도 어느 쪽이 걷혔는지 아무도 모르게 된다.
|
|
10
|
+
*
|
|
11
|
+
* 걸리면 **조용히 `null`** 이다 — 터지지도 찍지도 않는다. 옛 값이 그대로 들어오면
|
|
12
|
+
* 기기가 안 골라지는 꼴로 보이니, 소비자는 **코어에 넘기기 전에** 옮겨 둔다.
|
|
13
|
+
*/
|
|
14
|
+
const toSide = (d) => (d === 'left' || d === 'right' ? d : null);
|
|
5
15
|
/**
|
|
6
16
|
* 보청기 기기 등록 / 이력 관리 상태를 캡슐화한 훅.
|
|
7
17
|
*/
|
|
@@ -10,15 +20,17 @@ const toSide = (d) => (d === '좌' || d === '우' ? d : null);
|
|
|
10
20
|
*/
|
|
11
21
|
export function useDeviceManagement() {
|
|
12
22
|
const [showDevice, setShowDevice] = useState(false);
|
|
13
|
-
const [deviceDirection, setDeviceDirection] = useState('
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
const [deviceDirection, setDeviceDirection] = useState('left');
|
|
24
|
+
/* 열쇠는 `Side`(= `DeviceSide`) **값**(`'left'`·`'right'`) — 서버의 `side` 열에 오가는 자료다.
|
|
25
|
+
이름도 값도 영어다 — 값은 2026-10-13 사장님 결정으로 옮겼다(`'좌'`·`'우'` → `'left'`·`'right'`). */
|
|
26
|
+
const [devices, setDevices] = useState({ left: null, right: null });
|
|
27
|
+
const [deviceHistory, setDeviceHistory] = useState({ left: [], right: [] });
|
|
16
28
|
const [showDeviceList, setShowDeviceList] = useState(false);
|
|
17
|
-
const [listForSide, setListForSide] = useState('
|
|
29
|
+
const [listForSide, setListForSide] = useState('left');
|
|
18
30
|
// form.direction은 각 팝업(DeviceRegistrationPopup의 방향 드롭다운, DeviceListPopup의
|
|
19
|
-
//
|
|
31
|
+
// left/right 탭)에서 사용자가 실제로 고른 값이다 — deviceDirection/listForSide는 팝업을 열 때의
|
|
20
32
|
// 초기값일 뿐이라 팝업 내부에서 방향을 바꾸면 더 이상 실제 선택과 일치하지 않는다.
|
|
21
|
-
// 방향이 비었거나 '
|
|
33
|
+
// 방향이 비었거나 'left'·'right' 밖이면 **아무것도 바꾸지 않는다** — 예전에는 검사 없이 캐스팅해
|
|
22
34
|
// devices·deviceHistory 에 없는 열쇠가 생겼고 prev[side] 가 undefined 라 이력 펼치기에서 터졌다.
|
|
23
35
|
// 팝업은 닫지 않는다(사용자가 방향을 고르고 다시 제출할 수 있게).
|
|
24
36
|
const handleDeviceSubmit = useCallback((form) => {
|
|
@@ -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
|
}
|