@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
|
@@ -110,6 +110,16 @@
|
|
|
110
110
|
"금",
|
|
111
111
|
"토"
|
|
112
112
|
],
|
|
113
|
+
"weekOrdinal": [
|
|
114
|
+
"첫째",
|
|
115
|
+
"둘째",
|
|
116
|
+
"셋째",
|
|
117
|
+
"넷째",
|
|
118
|
+
"다섯째",
|
|
119
|
+
"여섯째",
|
|
120
|
+
"일곱째"
|
|
121
|
+
],
|
|
122
|
+
"weekLabel": "{{ord}}주",
|
|
113
123
|
"yearMonth": "{{year}}년 {{month}}월",
|
|
114
124
|
"prevMonth": "이전 달",
|
|
115
125
|
"addOnDate": "{{date}}에 방문 등록",
|
|
@@ -899,6 +909,7 @@
|
|
|
899
909
|
"modifier": "수식언(부사, 관형사)"
|
|
900
910
|
},
|
|
901
911
|
"wordType": "유형",
|
|
912
|
+
"field": "의미범주",
|
|
902
913
|
"type": {
|
|
903
914
|
"native": "고유어",
|
|
904
915
|
"sino": "한자어",
|
|
@@ -1048,7 +1059,8 @@
|
|
|
1048
1059
|
"removeDevice": "착용 해제",
|
|
1049
1060
|
"type": {
|
|
1050
1061
|
"hearingAid": "보청기",
|
|
1051
|
-
"cochlearImplant": "인공와우"
|
|
1062
|
+
"cochlearImplant": "인공와우",
|
|
1063
|
+
"other": "기타"
|
|
1052
1064
|
}
|
|
1053
1065
|
},
|
|
1054
1066
|
"manager": {
|
|
@@ -19,7 +19,16 @@ export interface WordSearchResultWord {
|
|
|
19
19
|
export interface WordSearchResultItem {
|
|
20
20
|
id: string;
|
|
21
21
|
words: WordSearchResultWord[];
|
|
22
|
+
/**
|
|
23
|
+
* 갈린 자리 — **옛 한글 이름**(`'초성'`·`'모음'`·`'종성'`·`'활음'`)이다.
|
|
24
|
+
*
|
|
25
|
+
* @deprecated `diffSectionCode` 를 쓴다 — 같은 자리를 코드로 가리킨다. 2026-10-13 안쪽을
|
|
26
|
+
* 코드로 옮겼지만 **소비자가 이 값으로 묶음을 가른다**(말귀 `WordPairsPage` 의 `GROUP_ORDER`).
|
|
27
|
+
* 혼자 바꾸면 그쪽 묶음이 조용히 빈다. 소비자가 옮긴 뒤 걷는다.
|
|
28
|
+
*/
|
|
22
29
|
diffSection?: string;
|
|
30
|
+
/** 갈린 자리 — 코드(`'onset'`·`'vowel'`·`'coda'`·`'glide'`). 이쪽이 정본이다. */
|
|
31
|
+
diffSectionCode?: import('../utils/phonemeSectionCodes').PhonemeSectionCode;
|
|
23
32
|
diffHasEmpty?: boolean;
|
|
24
33
|
}
|
|
25
34
|
/**
|
|
@@ -20,19 +20,19 @@ const _graphs = new WeakMap();
|
|
|
20
20
|
const _desired = new WeakMap();
|
|
21
21
|
const _pending = new WeakMap();
|
|
22
22
|
let _unlockInstalled = false;
|
|
23
|
-
const
|
|
23
|
+
const clamp01 = (v) => Math.max(0, Math.min(1, Number.isFinite(v) ? v : 0));
|
|
24
24
|
/** 첫 사용자 제스처(및 이후 제스처)마다 멈춘 컨텍스트를 깨운다 — iOS 는 제스처 안에서만 resume 이 먹는다 */
|
|
25
25
|
function installGestureUnlock() {
|
|
26
26
|
if (_unlockInstalled || typeof document === 'undefined')
|
|
27
27
|
return;
|
|
28
28
|
_unlockInstalled = true;
|
|
29
|
-
const
|
|
29
|
+
const resumeCtx = () => { try {
|
|
30
30
|
if (_ctx && _ctx.state !== 'running')
|
|
31
31
|
void _ctx.resume();
|
|
32
32
|
}
|
|
33
33
|
catch { /* noop */ } };
|
|
34
34
|
for (const type of ['pointerdown', 'touchend', 'keydown']) {
|
|
35
|
-
document.addEventListener(type,
|
|
35
|
+
document.addEventListener(type, resumeCtx, { capture: true, passive: true });
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
function getContext() {
|
|
@@ -68,7 +68,7 @@ function build(ctx, el) {
|
|
|
68
68
|
analyser.fftSize = 1024;
|
|
69
69
|
analyser.smoothingTimeConstant = 0.6;
|
|
70
70
|
// 태우기 직전의 요소 볼륨을 gain 으로 옮기고 요소는 1 로 — 순서가 바뀌면 한 순간 두 번 곱해진다
|
|
71
|
-
gain.gain.value =
|
|
71
|
+
gain.gain.value = clamp01(_desired.get(el) ?? el.volume);
|
|
72
72
|
source.connect(gain);
|
|
73
73
|
gain.connect(analyser);
|
|
74
74
|
gain.connect(ctx.destination);
|
|
@@ -137,7 +137,7 @@ export function getElementAnalyser(el) {
|
|
|
137
137
|
* 불러 램프를 만들어도 된다.
|
|
138
138
|
*/
|
|
139
139
|
export function setElementVolume(el, volume) {
|
|
140
|
-
const v =
|
|
140
|
+
const v = clamp01(volume);
|
|
141
141
|
_desired.set(el, v);
|
|
142
142
|
const graph = _graphs.get(el);
|
|
143
143
|
if (graph) {
|
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
* 문장의 앞 무음은 진짜 그 소리의 일부이고, 앞뒤 뜸((74))이 그 위에서 셈된다.
|
|
7
7
|
* @public
|
|
8
8
|
*/
|
|
9
|
-
export declare function computePeaksFromUrl(url: string, bars: number, windowSeconds?: number,
|
|
9
|
+
export declare function computePeaksFromUrl(url: string, bars: number, windowSeconds?: number, skipLeadingSilence?: boolean): Promise<number[] | null>;
|
|
@@ -26,10 +26,10 @@ import { getErrorMessage } from './errorMessage';
|
|
|
26
26
|
*
|
|
27
27
|
* 0.001 은 디더까지 잡고 0.02 는 진짜 작은 소리를 잘라 낸다 — 가운데를 쓴다.
|
|
28
28
|
*/
|
|
29
|
-
const
|
|
29
|
+
const SILENCE_LEVEL = 0.01;
|
|
30
30
|
/** 아무리 조용한 파일이라도 이만큼 넘게는 안 건너뛴다 — 통째로 미끄러지지 않게 */
|
|
31
|
-
const
|
|
32
|
-
async function decodeAndComputePeaks(arrayBuffer, bars, windowSeconds,
|
|
31
|
+
const SKIP_LIMIT_SEC = 0.3;
|
|
32
|
+
async function decodeAndComputePeaks(arrayBuffer, bars, windowSeconds, skipLeadingSilence = false) {
|
|
33
33
|
/* 컨텍스트는 **어느 길로 나가든** 닫는다 — 디코딩 실패(404·만료 주소의 HTML)·채널 없음에서 닫지 않아
|
|
34
34
|
파형 하나 실패할 때마다 AudioContext 가 하나씩 남았다(브라우저 동시 한도에 닿으면 이후 파형이 모두 실패). */
|
|
35
35
|
let audioCtx = null;
|
|
@@ -42,24 +42,24 @@ async function decodeAndComputePeaks(arrayBuffer, bars, windowSeconds, 앞무음
|
|
|
42
42
|
return null;
|
|
43
43
|
/* **앞머리 무음을 건너뛴다**(소음처럼 루프되는 바탕에서만 켠다). 소리 있는 첫 자리를 찾아
|
|
44
44
|
거기서부터 창을 연다 — 그래야 그린 결이 **실제로 들리는 결**이 된다. */
|
|
45
|
-
let
|
|
46
|
-
if (
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
while (
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
45
|
+
let offset = 0;
|
|
46
|
+
if (skipLeadingSilence) {
|
|
47
|
+
const limit = Math.floor(SKIP_LIMIT_SEC * decoded.sampleRate);
|
|
48
|
+
const end = Math.min(channelData.length, limit);
|
|
49
|
+
while (offset < end && Math.abs(channelData[offset]) <= SILENCE_LEVEL)
|
|
50
|
+
offset++;
|
|
51
|
+
if (offset >= end)
|
|
52
|
+
offset = 0; // 통째로 조용한 파일 — 건너뛰지 않는다
|
|
53
53
|
}
|
|
54
|
-
const
|
|
54
|
+
const remaining = channelData.length - offset;
|
|
55
55
|
const total = (windowSeconds && windowSeconds > 0)
|
|
56
|
-
? Math.min(
|
|
57
|
-
:
|
|
56
|
+
? Math.min(remaining, Math.floor(windowSeconds * decoded.sampleRate))
|
|
57
|
+
: remaining;
|
|
58
58
|
const usableLen = Math.max(bars, total);
|
|
59
59
|
const blockSize = Math.floor(usableLen / bars) || 1;
|
|
60
60
|
const peaks = new Array(bars).fill(0).map((_, i) => {
|
|
61
|
-
const start =
|
|
62
|
-
const end = Math.min(start + blockSize,
|
|
61
|
+
const start = offset + i * blockSize;
|
|
62
|
+
const end = Math.min(start + blockSize, offset + usableLen, channelData.length);
|
|
63
63
|
let max = 0;
|
|
64
64
|
for (let j = start; j < end; j++) {
|
|
65
65
|
const v = Math.abs(channelData[j]);
|
|
@@ -89,14 +89,14 @@ async function decodeAndComputePeaks(arrayBuffer, bars, windowSeconds, 앞무음
|
|
|
89
89
|
* 문장의 앞 무음은 진짜 그 소리의 일부이고, 앞뒤 뜸((74))이 그 위에서 셈된다.
|
|
90
90
|
* @public
|
|
91
91
|
*/
|
|
92
|
-
export async function computePeaksFromUrl(url, bars, windowSeconds,
|
|
92
|
+
export async function computePeaksFromUrl(url, bars, windowSeconds, skipLeadingSilence = false) {
|
|
93
93
|
try {
|
|
94
94
|
const resp = await fetch(url);
|
|
95
95
|
// 404·만료 주소는 소리가 아니다 — 디코딩까지 가지 않는다
|
|
96
96
|
if (resp.ok === false)
|
|
97
97
|
return null;
|
|
98
98
|
const arrayBuffer = await resp.arrayBuffer();
|
|
99
|
-
return await decodeAndComputePeaks(arrayBuffer, bars, windowSeconds,
|
|
99
|
+
return await decodeAndComputePeaks(arrayBuffer, bars, windowSeconds, skipLeadingSilence);
|
|
100
100
|
}
|
|
101
101
|
catch (e) {
|
|
102
102
|
try {
|
|
@@ -20,9 +20,9 @@ export function columnsFitting(width, cardWidth, gap) {
|
|
|
20
20
|
export function balancedColumns(count, perRow) {
|
|
21
21
|
if (!(count > 0))
|
|
22
22
|
return 1;
|
|
23
|
-
const
|
|
24
|
-
if (count <=
|
|
23
|
+
const perRowInt = Math.max(1, Math.floor(perRow));
|
|
24
|
+
if (count <= perRowInt)
|
|
25
25
|
return count;
|
|
26
|
-
const
|
|
27
|
-
return Math.max(1, Math.ceil(count /
|
|
26
|
+
const rowCount = Math.ceil(count / perRowInt);
|
|
27
|
+
return Math.max(1, Math.ceil(count / rowCount));
|
|
28
28
|
}
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
const SKEW_KEY = 'chunk-reload-ts';
|
|
32
32
|
/** 이 시간 안에 다시 실패하면 배포 skew 가 아니라 진짜 오류로 본다(무한 새로고침 방지). */
|
|
33
33
|
const COOLDOWN_MS = 10_000;
|
|
34
|
-
function
|
|
34
|
+
function lastAt() {
|
|
35
35
|
try {
|
|
36
36
|
return Number(sessionStorage.getItem(SKEW_KEY) || 0);
|
|
37
37
|
}
|
|
@@ -48,7 +48,7 @@ function 마지막시각() {
|
|
|
48
48
|
* @public
|
|
49
49
|
*/
|
|
50
50
|
export function recoverFromSkew(now = Date.now()) {
|
|
51
|
-
if (now -
|
|
51
|
+
if (now - lastAt() <= COOLDOWN_MS)
|
|
52
52
|
return false;
|
|
53
53
|
try {
|
|
54
54
|
sessionStorage.setItem(SKEW_KEY, String(now));
|
|
@@ -65,7 +65,7 @@ export function recoverFromSkew(now = Date.now()) {
|
|
|
65
65
|
* 무엇을 그릴지 정하는 데 쓴다 — **아무것도 안 그리는 것이 맞다.** 곧 판이 바뀐다.
|
|
66
66
|
*/
|
|
67
67
|
export function skewReloadPending(now = Date.now()) {
|
|
68
|
-
const last =
|
|
68
|
+
const last = lastAt();
|
|
69
69
|
return last > 0 && now - last <= COOLDOWN_MS;
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
@@ -97,18 +97,18 @@ export function skewReloadPending(now = Date.now()) {
|
|
|
97
97
|
export function installSkewRecovery() {
|
|
98
98
|
if (typeof window === 'undefined')
|
|
99
99
|
return () => { };
|
|
100
|
-
const
|
|
100
|
+
const onChunkError = (e) => { if (recoverFromSkew())
|
|
101
101
|
e.preventDefault(); };
|
|
102
102
|
/* 스타일시트만 본다 — 그림 하나가 깨졌다고 판을 새로 받으면 안 된다 */
|
|
103
|
-
const
|
|
103
|
+
const onStyleError = (e) => {
|
|
104
104
|
const el = e.target;
|
|
105
105
|
if (el instanceof HTMLLinkElement && el.rel === 'stylesheet')
|
|
106
106
|
recoverFromSkew();
|
|
107
107
|
};
|
|
108
|
-
window.addEventListener('vite:preloadError',
|
|
109
|
-
window.addEventListener('error',
|
|
108
|
+
window.addEventListener('vite:preloadError', onChunkError);
|
|
109
|
+
window.addEventListener('error', onStyleError, true);
|
|
110
110
|
return () => {
|
|
111
|
-
window.removeEventListener('vite:preloadError',
|
|
112
|
-
window.removeEventListener('error',
|
|
111
|
+
window.removeEventListener('vite:preloadError', onChunkError);
|
|
112
|
+
window.removeEventListener('error', onStyleError, true);
|
|
113
113
|
};
|
|
114
114
|
}
|
|
@@ -12,7 +12,7 @@ export async function copyText(text) {
|
|
|
12
12
|
if (typeof document !== 'undefined') {
|
|
13
13
|
// 숨긴 textarea 에 포커스를 옮기므로, 끝나면 원래 자리로 돌려준다 — 안 돌리면 복사 단추를
|
|
14
14
|
// 누른 뒤 키보드 포커스가 body 로 떨어져 스크린리더·Tab 이동이 처음부터 다시 시작된다.
|
|
15
|
-
const
|
|
15
|
+
const previousFocus = document.activeElement;
|
|
16
16
|
let ta = null;
|
|
17
17
|
try {
|
|
18
18
|
ta = document.createElement('textarea');
|
|
@@ -38,9 +38,9 @@ export async function copyText(text) {
|
|
|
38
38
|
// 성공·실패·던짐 어느 길이든 textarea 를 거두고 포커스를 되돌린다
|
|
39
39
|
if (ta?.parentNode)
|
|
40
40
|
ta.parentNode.removeChild(ta);
|
|
41
|
-
if (
|
|
41
|
+
if (previousFocus && previousFocus !== document.activeElement && typeof previousFocus.focus === 'function' && previousFocus.isConnected) {
|
|
42
42
|
try {
|
|
43
|
-
|
|
43
|
+
previousFocus.focus({ preventScroll: true });
|
|
44
44
|
}
|
|
45
45
|
catch { /* 무시 */ }
|
|
46
46
|
}
|
|
@@ -26,12 +26,12 @@ function mulberry32(seed) {
|
|
|
26
26
|
// shuffle/sample은 공용 utils/array로 통일(시드 rng 지원 동일). 여기선 sampleArray 이름만 유지해 내부에서 사용.
|
|
27
27
|
const sampleArray = sample;
|
|
28
28
|
/** KST 날짜 'YYYY-MM-DD' → 날 번호(1970-01-01 = 0). 홀짝으로 이틀마다 번갈아 쓸 반쪽을 고른다. */
|
|
29
|
-
const
|
|
29
|
+
const dayNumber = (ymd) => {
|
|
30
30
|
const [y, m, d] = ymd.split('-').map(Number);
|
|
31
31
|
return Math.round(Date.UTC(y, (m || 1) - 1, d || 1) / 86_400_000);
|
|
32
32
|
};
|
|
33
33
|
/** 항목을 가리키는 열쇠 — 자료의 id(없으면 name). 자료가 늘거나 차례가 바뀌어도 그 항목의 반쪽이 안 바뀌게 */
|
|
34
|
-
const
|
|
34
|
+
const itemKey = (it, i) => {
|
|
35
35
|
const o = (it ?? {});
|
|
36
36
|
return String(o.id ?? o.name ?? i);
|
|
37
37
|
};
|
|
@@ -47,23 +47,23 @@ const 항목열쇠 = (it, i) => {
|
|
|
47
47
|
* 정하고, 날 번호의 홀짝에 맞는 반쪽에서만 뽑는다** — 이웃한 두 날은 늘 다른 반쪽이라 겹치지 않고, 어제 보기는 어제 계산과 같다.
|
|
48
48
|
* 두 반쪽이 모두 필요한 개수 이상일 때만(후보가 충분할 때) 이렇게 하고, 모자라면 전처럼 전체에서 뽑는다(빈 세트로 두지 않는다).
|
|
49
49
|
*/
|
|
50
|
-
function
|
|
50
|
+
function typeSet(pool, n, userId, ymd, type) {
|
|
51
51
|
if (!pool.length || n <= 0)
|
|
52
52
|
return [];
|
|
53
53
|
const rng = mulberry32(hashSeed(`${userId}|${ymd}|${type}`));
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
return sampleArray((
|
|
54
|
+
const side = dayNumber(ymd) & 1;
|
|
55
|
+
const thisSide = pool.filter((it, i) => ((hashSeed(`${userId}|${type}|${itemKey(it, i)}`) >>> 7) & 1) === side);
|
|
56
|
+
const enough = thisSide.length >= n && pool.length - thisSide.length >= n;
|
|
57
|
+
return sampleArray((enough ? thisSide : pool), n, rng);
|
|
58
58
|
}
|
|
59
59
|
// (user, 날짜)로 결정적 추출 → 기기/세션 무관 동일 선택. 유형마다 시드가 따로다(위 유형세트).
|
|
60
60
|
function createSelection(counts, userId, ymd) {
|
|
61
|
-
const
|
|
61
|
+
const countOf = (k) => Math.max(0, counts[k] || 0);
|
|
62
62
|
return {
|
|
63
|
-
LISTEN_IN_NOISE:
|
|
64
|
-
SHORT_STORY:
|
|
65
|
-
LONG_STORY:
|
|
66
|
-
ORDERING_SENTENCE:
|
|
63
|
+
LISTEN_IN_NOISE: typeSet(NOISE_DATAS, countOf('LISTEN_IN_NOISE'), userId, ymd, 'LISTEN_IN_NOISE'),
|
|
64
|
+
SHORT_STORY: typeSet(SHORT_STORIES, countOf('SHORT_STORY'), userId, ymd, 'SHORT_STORY'),
|
|
65
|
+
LONG_STORY: typeSet(LONG_STORIES, countOf('LONG_STORY'), userId, ymd, 'LONG_STORY'),
|
|
66
|
+
ORDERING_SENTENCE: typeSet(ORDER_SETS, countOf('ORDERING_SENTENCE'), userId, ymd, 'ORDERING_SENTENCE'),
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
export function hasItems(s) {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
const KST = 'Asia/Seoul';
|
|
19
19
|
/* 시간대 없는 벽시계 시각 — 'YYYY-MM-DDTHH:mm[:ss[.fff…]]' 또는 가운데가 빈칸('YYYY-MM-DD HH:mm:ss', Postgres timestamp 꼴).
|
|
20
20
|
뒤돌아보기 없이(iOS 15 Safari) 끝까지 맞춘다 — 뒤에 Z·±hh:mm 이 붙으면 걸리지 않는다. */
|
|
21
|
-
const
|
|
21
|
+
const WALL_CLOCK = /^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2})(?::(\d{2})(?:\.(\d+))?)?$/;
|
|
22
22
|
/**
|
|
23
23
|
* 날짜·시각 값을 순간(Date)으로 읽는 **이 파일의 공용 파서.** 못 읽는 값(빈 값·깨진 글자)이면 null.
|
|
24
24
|
*
|
|
@@ -28,13 +28,13 @@ const 벽시계시각 = /^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2})(?::(\d{2})(?:\.(\
|
|
|
28
28
|
* 날짜만 있는 'YYYY-MM-DD' 는 지금 규칙(UTC 자정 = KST 같은 날 9시)을 그대로 따른다.
|
|
29
29
|
* Date·숫자·오프셋 있는 글자는 그대로.
|
|
30
30
|
*/
|
|
31
|
-
function
|
|
31
|
+
function toInstant(when) {
|
|
32
32
|
let d;
|
|
33
33
|
if (when instanceof Date)
|
|
34
34
|
d = when;
|
|
35
35
|
else if (typeof when === 'string') {
|
|
36
36
|
const s = when.trim();
|
|
37
|
-
const m =
|
|
37
|
+
const m = WALL_CLOCK.exec(s);
|
|
38
38
|
/* 소수 초는 세 자리까지만 — 여섯 자리(Postgres)는 엔진마다 읽기가 갈린다 */
|
|
39
39
|
d = new Date(m ? `${m[1]}T${m[2]}:${m[3] ?? '00'}${m[4] ? `.${m[4].slice(0, 3).padEnd(3, '0')}` : ''}+09:00` : s);
|
|
40
40
|
}
|
|
@@ -44,16 +44,16 @@ function 순간으로(when) {
|
|
|
44
44
|
}
|
|
45
45
|
/* 못 읽는 값(빈 문자열·깨진 날짜)이면 null — 아래 형식 함수들이 화면에 "Invalid Date" 를 그대로 내던 것을 빈 글자로 막는다
|
|
46
46
|
(같은 파일의 toKstYmd·formatYmd 는 이미 막고 있었다) */
|
|
47
|
-
const
|
|
47
|
+
const parsed = (iso) => toInstant(iso);
|
|
48
48
|
export function formatDateKo(iso) {
|
|
49
|
-
const d =
|
|
49
|
+
const d = parsed(iso);
|
|
50
50
|
if (!d)
|
|
51
51
|
return '';
|
|
52
52
|
return d.toLocaleDateString('ko-KR', { timeZone: KST, year: 'numeric', month: 'short', day: 'numeric' });
|
|
53
53
|
}
|
|
54
54
|
/** "2024년 1월 15일 오후 03:30" (long month + time) */
|
|
55
55
|
export function formatDateLongKo(iso) {
|
|
56
|
-
const d =
|
|
56
|
+
const d = parsed(iso);
|
|
57
57
|
if (!d)
|
|
58
58
|
return '';
|
|
59
59
|
return d.toLocaleString('ko-KR', {
|
|
@@ -70,7 +70,7 @@ export function formatDateLongKo(iso) {
|
|
|
70
70
|
*/
|
|
71
71
|
export function formatDateLocale(iso, lang) {
|
|
72
72
|
const locale = lang.startsWith('ko') ? 'ko-KR' : 'en-US';
|
|
73
|
-
const d =
|
|
73
|
+
const d = parsed(iso);
|
|
74
74
|
if (!d)
|
|
75
75
|
return '';
|
|
76
76
|
return d.toLocaleDateString(locale, { timeZone: KST, year: 'numeric', month: 'short', day: 'numeric' });
|
|
@@ -78,7 +78,7 @@ export function formatDateLocale(iso, lang) {
|
|
|
78
78
|
/** formatDateLongKo의 로케일 인식 버전 — en: "January 15, 2024, 03:30 PM" */
|
|
79
79
|
export function formatDateLongLocale(iso, lang) {
|
|
80
80
|
const locale = lang.startsWith('ko') ? 'ko-KR' : 'en-US';
|
|
81
|
-
const d =
|
|
81
|
+
const d = parsed(iso);
|
|
82
82
|
if (!d)
|
|
83
83
|
return '';
|
|
84
84
|
return d.toLocaleString(locale, {
|
|
@@ -89,7 +89,7 @@ export function formatDateLongLocale(iso, lang) {
|
|
|
89
89
|
}
|
|
90
90
|
/** "2024. 01. 15. 오후 03:30" (2-digit month + time) */
|
|
91
91
|
export function formatDatetimeKo(iso) {
|
|
92
|
-
const d =
|
|
92
|
+
const d = parsed(iso);
|
|
93
93
|
if (!d)
|
|
94
94
|
return '';
|
|
95
95
|
return d.toLocaleString('ko-KR', {
|
|
@@ -110,7 +110,7 @@ export function formatDatetimeKo(iso) {
|
|
|
110
110
|
export function toKstHm(when) {
|
|
111
111
|
if (when === null || when === undefined || when === '')
|
|
112
112
|
return '';
|
|
113
|
-
const d =
|
|
113
|
+
const d = toInstant(when);
|
|
114
114
|
if (!d)
|
|
115
115
|
return '';
|
|
116
116
|
return new Intl.DateTimeFormat('en-GB', {
|
|
@@ -177,7 +177,7 @@ export function parseLocalYmd(ymd) {
|
|
|
177
177
|
export function toKstYmd(when) {
|
|
178
178
|
if (when === null || when === undefined || when === '')
|
|
179
179
|
return '';
|
|
180
|
-
const d =
|
|
180
|
+
const d = toInstant(when);
|
|
181
181
|
if (!d)
|
|
182
182
|
return '';
|
|
183
183
|
return new Intl.DateTimeFormat('fr-CA', { timeZone: 'Asia/Seoul' }).format(d);
|
|
@@ -191,7 +191,7 @@ export function toKstYmd(when) {
|
|
|
191
191
|
export function formatRelativeTime(input, t) {
|
|
192
192
|
if (!input)
|
|
193
193
|
return '';
|
|
194
|
-
const d =
|
|
194
|
+
const d = toInstant(input);
|
|
195
195
|
if (!d)
|
|
196
196
|
return '';
|
|
197
197
|
const then = d.getTime();
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* **왜 한 번만인가** — 컴포넌트는 다시 그려진다. 그릴 때마다 찍으면 콘솔이
|
|
9
9
|
* 넘쳐서 진짜 봐야 할 것이 묻힌다.
|
|
10
10
|
*/
|
|
11
|
-
const
|
|
11
|
+
const printed = new Set();
|
|
12
12
|
export const isDev = (() => {
|
|
13
13
|
try {
|
|
14
14
|
if (import.meta.env?.DEV)
|
|
@@ -29,12 +29,12 @@ export const isDev = (() => {
|
|
|
29
29
|
})();
|
|
30
30
|
/** `key` 하나당 한 번만 찍는다. 운영 빌드에서는 아무 일도 하지 않는다. */
|
|
31
31
|
export function devWarnOnce(key, ...args) {
|
|
32
|
-
if (!isDev ||
|
|
32
|
+
if (!isDev || printed.has(key))
|
|
33
33
|
return;
|
|
34
|
-
|
|
34
|
+
printed.add(key);
|
|
35
35
|
console.warn(...args);
|
|
36
36
|
}
|
|
37
37
|
/** 시험용 — 기록을 비운다. 운영 코드에서 부를 일은 없다. */
|
|
38
38
|
export function __resetDevWarnOnce() {
|
|
39
|
-
|
|
39
|
+
printed.clear();
|
|
40
40
|
}
|
|
@@ -1,13 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 기기 종류(보청기·인공와우·기타)
|
|
2
|
+
* 기기 종류(보청기·인공와우·기타) — **이름과 색을 한 자리에서 정한다.**
|
|
3
3
|
*
|
|
4
|
-
* 표(DeviceSideTable)와 '사용중인 기기' 패널(PanelRowDevice)이 같은 값을 각자
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 표(`DeviceSideTable`)와 '사용중인 기기' 패널(`PanelRowDevice`)이 같은 값을 각자 그리므로
|
|
5
|
+
* 규칙을 한 곳에 둔다 — 두 곳에서 따로 정하면 같은 인공와우가 한쪽은 파랑, 한쪽은 초록으로
|
|
6
|
+
* 떠서 다른 것인 줄 알게 된다.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* **색만 모여 있고 이름은 안 모여 있었다** — `deviceTypeKeys`·`deviceTypeLabel` 이 그 두
|
|
9
|
+
* 파일에 **주석까지 똑같이 두 벌** 있었다(2026-10-13 말귀가 짚었다). 색에 적어 둔 그 까닭이
|
|
10
|
+
* 이름에도 그대로 들어맞는 자리다. 모았다.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
+
* ## 값은 **코드**다 — 옛 한글 값도 받는다
|
|
13
|
+
*
|
|
14
|
+
* 저장값(`managed_device.type`)이 `'보청기'` 에서 `'hearingAid'` 로 옮겨 가는 중이다
|
|
15
|
+
* (사장님 지시 "저장된 한글 값도 다 영어로"). 앱이 판을 돌리는 때와 이 판이 깔리는 때가
|
|
16
|
+
* 어긋나고, 옛 자료를 보는 다른 기관이 있을 수 있어 **둘 다 받는다**.
|
|
17
|
+
*
|
|
18
|
+
* 받지 않으면 **터지지 않고 조용히 틀린다** — 배지에 `hearingAid` 가 날것으로 뜨고
|
|
19
|
+
* (`defaultValue: device` 로 떨어진다), 색이 둘 다 무채색이 되어 종류를 색으로 가르던 것이
|
|
20
|
+
* 사라진다. 눈으로만 잡히는 꼴이다.
|
|
21
|
+
*/
|
|
22
|
+
export type DeviceKind = 'hearingAid' | 'cochlearImplant' | 'other';
|
|
23
|
+
/**
|
|
24
|
+
* 저장값을 종류 코드로 — 모르는 값은 `null`.
|
|
25
|
+
*
|
|
26
|
+
* **모르는 것을 `'other'` 로 접지 않는다.** 기타는 "기타" 라고 고른 값이고, 모르는 값은
|
|
27
|
+
* 아직 뜻을 모르는 것이다. 둘을 섞으면 새 종류가 생긴 날 조용히 '기타' 로 보인다.
|
|
28
|
+
*/
|
|
29
|
+
export declare function deviceKindOf(device: string | null | undefined): DeviceKind | null;
|
|
30
|
+
/**
|
|
31
|
+
* 배지 색. 목록에 없는 값·빈 값은 `'default'`(무채색)로 떨어뜨린다 —
|
|
32
|
+
* **모르는 종류를 아는 색으로 칠하지 않는다.**
|
|
12
33
|
*/
|
|
13
34
|
export declare function deviceKindVariant(device: string | null | undefined): string;
|
|
35
|
+
/**
|
|
36
|
+
* 배지에 적을 이름 — **지금 언어로**. 사전(`device.type.*`)이 쥔다.
|
|
37
|
+
*
|
|
38
|
+
* 모르는 값은 **저장된 그대로** 보인다. 숨기지 않는다 — 화면에 날것이 보이면 자료에 무엇이
|
|
39
|
+
* 들어 있는지 알 수 있고, 빈칸이면 아무것도 알 수 없다.
|
|
40
|
+
*/
|
|
41
|
+
export declare function deviceKindLabel(device: string | null | undefined, t: (k: string, o: {
|
|
42
|
+
defaultValue: string;
|
|
43
|
+
}) => string): string;
|
|
@@ -1,19 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 기기 종류(보청기·인공와우·기타)
|
|
2
|
+
* 기기 종류(보청기·인공와우·기타) — **이름과 색을 한 자리에서 정한다.**
|
|
3
3
|
*
|
|
4
|
-
* 표(DeviceSideTable)와 '사용중인 기기' 패널(PanelRowDevice)이 같은 값을 각자
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 표(`DeviceSideTable`)와 '사용중인 기기' 패널(`PanelRowDevice`)이 같은 값을 각자 그리므로
|
|
5
|
+
* 규칙을 한 곳에 둔다 — 두 곳에서 따로 정하면 같은 인공와우가 한쪽은 파랑, 한쪽은 초록으로
|
|
6
|
+
* 떠서 다른 것인 줄 알게 된다.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* **색만 모여 있고 이름은 안 모여 있었다** — `deviceTypeKeys`·`deviceTypeLabel` 이 그 두
|
|
9
|
+
* 파일에 **주석까지 똑같이 두 벌** 있었다(2026-10-13 말귀가 짚었다). 색에 적어 둔 그 까닭이
|
|
10
|
+
* 이름에도 그대로 들어맞는 자리다. 모았다.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
+
* ## 값은 **코드**다 — 옛 한글 값도 받는다
|
|
13
|
+
*
|
|
14
|
+
* 저장값(`managed_device.type`)이 `'보청기'` 에서 `'hearingAid'` 로 옮겨 가는 중이다
|
|
15
|
+
* (사장님 지시 "저장된 한글 값도 다 영어로"). 앱이 판을 돌리는 때와 이 판이 깔리는 때가
|
|
16
|
+
* 어긋나고, 옛 자료를 보는 다른 기관이 있을 수 있어 **둘 다 받는다**.
|
|
17
|
+
*
|
|
18
|
+
* 받지 않으면 **터지지 않고 조용히 틀린다** — 배지에 `hearingAid` 가 날것으로 뜨고
|
|
19
|
+
* (`defaultValue: device` 로 떨어진다), 색이 둘 다 무채색이 되어 종류를 색으로 가르던 것이
|
|
20
|
+
* 사라진다. 눈으로만 잡히는 꼴이다.
|
|
21
|
+
*/
|
|
22
|
+
/** 저장값 → 코드. 옛 한글 값도 받는다(위 머리말) */
|
|
23
|
+
const TO_KIND = {
|
|
24
|
+
hearingAid: 'hearingAid', cochlearImplant: 'cochlearImplant', other: 'other',
|
|
25
|
+
// 열쇠가 **저장된 값**이라 따옴표를 쥐어 둔다 — 이름이 아니다(이름은 영어로 쓴다)
|
|
26
|
+
'보청기': 'hearingAid', '인공와우': 'cochlearImplant', '기타': 'other',
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* 저장값을 종류 코드로 — 모르는 값은 `null`.
|
|
30
|
+
*
|
|
31
|
+
* **모르는 것을 `'other'` 로 접지 않는다.** 기타는 "기타" 라고 고른 값이고, 모르는 값은
|
|
32
|
+
* 아직 뜻을 모르는 것이다. 둘을 섞으면 새 종류가 생긴 날 조용히 '기타' 로 보인다.
|
|
33
|
+
*/
|
|
34
|
+
export function deviceKindOf(device) {
|
|
35
|
+
/* `device && …` 를 `?? null` 로 받으면 **빈 글자가 새어 나간다** — `'' && x` 는 `''` 이고
|
|
36
|
+
`?? ` 는 `null`·`undefined` 만 받으므로 `deviceKindOf('')` 가 `''` 를 돌려줬다(tsc 가 잡았다).
|
|
37
|
+
빈 값은 "종류가 없다" 이지 "빈 이름의 종류" 가 아니다. */
|
|
38
|
+
if (!device)
|
|
39
|
+
return null;
|
|
40
|
+
return TO_KIND[device] ?? null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 배지 색. 목록에 없는 값·빈 값은 `'default'`(무채색)로 떨어뜨린다 —
|
|
44
|
+
* **모르는 종류를 아는 색으로 칠하지 않는다.**
|
|
12
45
|
*/
|
|
13
46
|
export function deviceKindVariant(device) {
|
|
14
|
-
switch (device) {
|
|
15
|
-
case '
|
|
16
|
-
case '
|
|
47
|
+
switch (deviceKindOf(device)) {
|
|
48
|
+
case 'hearingAid': return 'main';
|
|
49
|
+
case 'cochlearImplant': return 'sub';
|
|
17
50
|
default: return 'default';
|
|
18
51
|
}
|
|
19
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* 배지에 적을 이름 — **지금 언어로**. 사전(`device.type.*`)이 쥔다.
|
|
55
|
+
*
|
|
56
|
+
* 모르는 값은 **저장된 그대로** 보인다. 숨기지 않는다 — 화면에 날것이 보이면 자료에 무엇이
|
|
57
|
+
* 들어 있는지 알 수 있고, 빈칸이면 아무것도 알 수 없다.
|
|
58
|
+
*/
|
|
59
|
+
export function deviceKindLabel(device, t) {
|
|
60
|
+
const kind = deviceKindOf(device);
|
|
61
|
+
return kind ? t(`device.type.${kind}`, { defaultValue: device ?? '' }) : (device ?? '');
|
|
62
|
+
}
|
|
@@ -6,10 +6,10 @@ import i18n from 'i18next';
|
|
|
6
6
|
* 여기서 `../i18n` 을 가져오면 utils 묶음이 사전 JSON 과 init 부작용을 끌어안으므로 기본
|
|
7
7
|
* 인스턴스를 직접 본다. 초기화 전의 `i18n.t` 는 undefined 를 돌려준다.
|
|
8
8
|
*/
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const s = typeof i18n?.t === 'function' ? i18n.t('common.unknownError', { defaultValue:
|
|
12
|
-
return typeof s === 'string' && s ? s :
|
|
9
|
+
const unknownError = () => {
|
|
10
|
+
const fallback = '알 수 없는 오류';
|
|
11
|
+
const s = typeof i18n?.t === 'function' ? i18n.t('common.unknownError', { defaultValue: fallback }) : undefined;
|
|
12
|
+
return typeof s === 'string' && s ? s : fallback;
|
|
13
13
|
};
|
|
14
14
|
/**
|
|
15
15
|
* 잡은 값에서 **사람이 읽을 수 있는 원인**을 꺼낸다.
|
|
@@ -33,7 +33,7 @@ const 알수없는오류 = () => {
|
|
|
33
33
|
export function getErrorMessage(e, fallback) {
|
|
34
34
|
// 기본 문구는 부를 때마다 지금 언어로 만든다(모듈 로드 때 한 번 정하면 언어 전환을 못 따른다)
|
|
35
35
|
if (fallback === undefined)
|
|
36
|
-
fallback =
|
|
36
|
+
fallback = unknownError();
|
|
37
37
|
if (e instanceof Error)
|
|
38
38
|
return e.message || fallback;
|
|
39
39
|
if (typeof e === 'string')
|
|
@@ -58,7 +58,7 @@ export function getErrorMessage(e, fallback) {
|
|
|
58
58
|
export const NETWORK_ERROR_MESSAGE = '네트워크 연결이 불안정합니다. 연결 상태를 확인한 뒤 다시 시도해 주세요.';
|
|
59
59
|
/* 화면에 내는 것은 지금 언어로(`common.networkError`, 사전에 없으면 위 한국어) — 상수만 돌려줘 영어 화면 토스트도 한국어였다.
|
|
60
60
|
상수는 옛 소비자가 견주는 값이라 그대로 둔다. */
|
|
61
|
-
const
|
|
61
|
+
const networkError = () => {
|
|
62
62
|
const s = typeof i18n?.t === 'function' ? i18n.t('common.networkError', { defaultValue: NETWORK_ERROR_MESSAGE }) : undefined;
|
|
63
63
|
return typeof s === 'string' && s ? s : NETWORK_ERROR_MESSAGE;
|
|
64
64
|
};
|
|
@@ -82,5 +82,5 @@ export function isNetworkErrorMessage(msg) {
|
|
|
82
82
|
* @public
|
|
83
83
|
*/
|
|
84
84
|
export function toUserErrorMessage(msg) {
|
|
85
|
-
return isNetworkErrorMessage(msg) ?
|
|
85
|
+
return isNetworkErrorMessage(msg) ? networkError() : msg;
|
|
86
86
|
}
|