@ihabdevteam/core 0.141.0 → 0.143.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 +73 -0
- package/dist/components.cjs.js +176 -176
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +182 -182
- package/dist/ihabdevteam-core.d.ts +81 -29
- package/dist/packages/core/src/components/BarChart.d.ts +1 -1
- package/dist/packages/core/src/components/BarChart.js +4 -3
- package/dist/packages/core/src/components/BottomNav.js +4 -0
- package/dist/packages/core/src/components/BulkBar.js +15 -4
- package/dist/packages/core/src/components/Button.js +10 -4
- package/dist/packages/core/src/components/DatePicker.js +5 -1
- package/dist/packages/core/src/components/Dialog.js +3 -1
- package/dist/packages/core/src/components/Dropdown.js +6 -2
- package/dist/packages/core/src/components/FileUpload.js +3 -1
- package/dist/packages/core/src/components/FilterPanel.js +3 -2
- package/dist/packages/core/src/components/PanelRow.js +5 -0
- package/dist/packages/core/src/components/PaymentPlan.d.ts +2 -2
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +3 -1
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +5 -2
- package/dist/packages/core/src/components/Popup/Popup.d.ts +6 -6
- package/dist/packages/core/src/components/Popup/Popup.js +11 -9
- package/dist/packages/core/src/components/Popup/customerDetailUtils.d.ts +1 -1
- package/dist/packages/core/src/components/Popup/customerDetailUtils.js +1 -1
- package/dist/packages/core/src/components/TextField.js +3 -1
- package/dist/packages/core/src/components/Tooltip/Tooltip.js +7 -1
- package/dist/packages/core/src/components/Training/TrainingBlock.js +2 -1
- package/dist/packages/core/src/components/Training/TrainingPagePopup.d.ts +1 -1
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +7 -1
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +52 -2
- package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +3 -1
- package/dist/packages/core/src/components/Training/WordTablePopup.js +9 -1
- package/dist/packages/core/src/components/Training/useAudioPreview.js +28 -3
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +19 -5
- package/dist/packages/core/src/components/Word/WordTableBlock.js +5 -2
- package/dist/packages/core/src/components/Word/WordTableButton.d.ts +3 -3
- package/dist/packages/core/src/components/Word/WordTableButton.js +1 -1
- package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +1 -1
- package/dist/packages/core/src/components/Word/wordTableBlockTypes.d.ts +2 -2
- package/dist/packages/core/src/components/barChartData.d.ts +3 -5
- package/dist/packages/core/src/components/barChartData.js +7 -3
- package/dist/packages/core/src/components/barChartTypes.d.ts +16 -3
- package/dist/packages/core/src/components/barChartTypes.js +27 -13
- package/dist/packages/core/src/components.d.ts +1 -0
- package/dist/packages/core/src/hooks/focusReturn.d.ts +4 -0
- package/dist/packages/core/src/hooks/focusReturn.js +27 -0
- package/dist/packages/core/src/hooks/inputModality.d.ts +2 -0
- package/dist/packages/core/src/hooks/inputModality.js +31 -0
- package/dist/packages/core/src/hooks/useDocumentTitle.d.ts +12 -0
- package/dist/packages/core/src/hooks/useDocumentTitle.js +7 -6
- package/dist/packages/core/src/hooks/useEditableList.js +3 -3
- package/dist/packages/core/src/hooks/useWordMeanings.js +6 -3
- package/dist/packages/core/src/hooks/useWordTrainingFilter.d.ts +2 -2
- package/dist/packages/core/src/hooks/useWordTrainingFilter.js +54 -17
- package/dist/packages/core/src/utils/date.d.ts +18 -0
- package/dist/packages/core/src/utils/date.js +23 -0
- package/dist/packages/core/src/utils/format.js +2 -1
- package/dist/packages/core/src/utils/imageConfig.d.ts +2 -0
- package/dist/packages/core/src/utils/imageConfig.js +34 -5
- package/dist/packages/core/src/utils/josa.js +2 -0
- package/dist/packages/core/src/utils/koreanSyllable.d.ts +5 -0
- package/dist/packages/core/src/utils/koreanSyllable.js +69 -43
- package/dist/packages/core/src/utils/phonemePairSearch.js +17 -0
- package/dist/packages/core/src/utils/textSort.d.ts +10 -0
- package/dist/packages/core/src/utils/textSort.js +16 -11
- package/dist/packages/core/src/utils/timeoutSignal.d.ts +5 -0
- package/dist/packages/core/src/utils/timeoutSignal.js +12 -0
- package/dist/packages/core/src/utils/ttsConfig.js +2 -1
- package/dist/packages/core/src/utils/wordContrast.js +6 -4
- package/dist/packages/core/src/utils/wordTrainingPool.js +14 -5
- package/dist/packages/core/src/utils.d.ts +1 -1
- package/dist/packages/core/src/utils.js +1 -1
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +2 -2
- package/package.json +1 -1
|
@@ -50,6 +50,11 @@ export declare function matchesChoseongSearch(text: string, search: string): boo
|
|
|
50
50
|
* 검색어에 종성 겹자모·비한글 등 위 종류가 아닌 문자가 있으면 일반 부분 문자열로 폴백한다.
|
|
51
51
|
*/
|
|
52
52
|
export declare function matchesWordQuery(word: string, query: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* @internal matchesWordQuery 의 검색어 쪽 준비(NFC·글자 분류)를 **한 번만** 해 두고 낱말마다 부르는 판별기.
|
|
55
|
+
* 목록 전체(2.3만 낱말)를 거를 때 낱말마다 검색어를 다시 펼치고 분류해, 키를 누를 때마다 14–36ms 걸렸다.
|
|
56
|
+
*/
|
|
57
|
+
export declare function makeWordQueryMatcher(query: string): (word: string) => boolean;
|
|
53
58
|
/** @public 복합 종성 index → 구성 자모 배열 (예: 9(ㄺ) → ['ㄹ','ㄱ']) */
|
|
54
59
|
export declare const JONG_DECOMPOSE: Record<number, string[]>;
|
|
55
60
|
/** @public 단어가 특정 자모(음소)를 어디든 (초성/종성/복합종성) 포함하는지 */
|
|
@@ -100,6 +100,9 @@ export function isAllChoseong(search) {
|
|
|
100
100
|
export function matchesChoseongSearch(text, search) {
|
|
101
101
|
if (!search)
|
|
102
102
|
return true;
|
|
103
|
+
/* 맥·PDF 에서 붙여넣은 글은 조각 한글(NFD, '가' = ᄀ+ᅡ)이라 완성 음절과 한 글자도 같지 않다 — 둘 다 NFC 로 맞춘다. */
|
|
104
|
+
text = text.normalize('NFC');
|
|
105
|
+
search = search.normalize('NFC');
|
|
103
106
|
if (isAllChoseong(search))
|
|
104
107
|
return toChoseong(text).includes(search);
|
|
105
108
|
return text.toLowerCase().includes(search.toLowerCase());
|
|
@@ -112,55 +115,69 @@ export function matchesChoseongSearch(text, search) {
|
|
|
112
115
|
* 검색어에 종성 겹자모·비한글 등 위 종류가 아닌 문자가 있으면 일반 부분 문자열로 폴백한다.
|
|
113
116
|
*/
|
|
114
117
|
export function matchesWordQuery(word, query) {
|
|
118
|
+
return makeWordQueryMatcher(query)(word);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* @internal matchesWordQuery 의 검색어 쪽 준비(NFC·글자 분류)를 **한 번만** 해 두고 낱말마다 부르는 판별기.
|
|
122
|
+
* 목록 전체(2.3만 낱말)를 거를 때 낱말마다 검색어를 다시 펼치고 분류해, 키를 누를 때마다 14–36ms 걸렸다.
|
|
123
|
+
*/
|
|
124
|
+
export function makeWordQueryMatcher(query) {
|
|
115
125
|
if (!query)
|
|
116
|
-
return true;
|
|
126
|
+
return () => true;
|
|
127
|
+
// 조각 한글(NFD) 붙여넣기는 '기타 문자' 로 분류돼 부분 문자열 비교로 새고 0건이 됐다 — 둘 다 NFC 로
|
|
128
|
+
query = query.normalize('NFC');
|
|
117
129
|
const q = [...query];
|
|
118
130
|
const isCho = (ch) => CHOSEONG.includes(ch);
|
|
119
131
|
const isJung = (ch) => JUNGSEONG.includes(ch);
|
|
120
132
|
// 각 글자 분류: 'cho'(자음/초성) | 'jung'(모음/중성) | 'syl'(완성 음절) | 'other'(그 외)
|
|
121
133
|
const kinds = q.map(ch => (isCho(ch) ? 'cho' : isJung(ch) ? 'jung' : parseSyllable(ch) ? 'syl' : 'other'));
|
|
134
|
+
const qSyl = q.map(ch => parseSyllable(ch));
|
|
122
135
|
// 영문은 대소문자를 가리지 않는다 — 옆 matchesChoseongSearch 와 같게('apple' 로 'Apple' 을 찾는다)
|
|
123
|
-
if (kinds.includes('other'))
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (!s || JUNGSEONG[s.jung] !== q[j]) {
|
|
140
|
-
ok = false;
|
|
141
|
-
break;
|
|
136
|
+
if (kinds.includes('other')) {
|
|
137
|
+
const lower = query.toLowerCase();
|
|
138
|
+
return (word) => word.normalize('NFC').toLowerCase().includes(lower);
|
|
139
|
+
}
|
|
140
|
+
return (word) => {
|
|
141
|
+
const w = [...word.normalize('NFC')];
|
|
142
|
+
for (let i = 0; i + q.length <= w.length; i++) {
|
|
143
|
+
let ok = true;
|
|
144
|
+
for (let j = 0; j < q.length; j++) {
|
|
145
|
+
const wc = w[i + j];
|
|
146
|
+
const k = kinds[j];
|
|
147
|
+
if (k === 'cho') {
|
|
148
|
+
if (firstChoseongOf(wc) !== q[j]) {
|
|
149
|
+
ok = false;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
142
152
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (qs && qs.jong === 0) {
|
|
147
|
-
// 받침 없는 글자는 초성+중성만 비교(받침 무관): '수' → 수·술·숙·순 …
|
|
148
|
-
const ws = parseSyllable(wc);
|
|
149
|
-
if (!ws || ws.cho !== qs.cho || ws.jung !== qs.jung) {
|
|
153
|
+
else if (k === 'jung') {
|
|
154
|
+
const s = parseSyllable(wc);
|
|
155
|
+
if (!s || JUNGSEONG[s.jung] !== q[j]) {
|
|
150
156
|
ok = false;
|
|
151
157
|
break;
|
|
152
158
|
}
|
|
153
159
|
}
|
|
154
|
-
else
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
else { // 'syl' — 완성 음절
|
|
161
|
+
const qs = qSyl[j];
|
|
162
|
+
if (qs && qs.jong === 0) {
|
|
163
|
+
// 받침 없는 글자는 초성+중성만 비교(받침 무관): '수' → 수·술·숙·순 …
|
|
164
|
+
const ws = parseSyllable(wc);
|
|
165
|
+
if (!ws || ws.cho !== qs.cho || ws.jung !== qs.jung) {
|
|
166
|
+
ok = false;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else if (wc !== q[j]) {
|
|
171
|
+
ok = false;
|
|
172
|
+
break;
|
|
173
|
+
} // 받침 있으면 정확 일치
|
|
174
|
+
}
|
|
158
175
|
}
|
|
176
|
+
if (ok)
|
|
177
|
+
return true;
|
|
159
178
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
return false;
|
|
179
|
+
return false;
|
|
180
|
+
};
|
|
164
181
|
}
|
|
165
182
|
// 복합 종성 자모 분해 (예: ㄺ → ㄹ+ㄱ)
|
|
166
183
|
/** @public 복합 종성 index → 구성 자모 배열 (예: 9(ㄺ) → ['ㄹ','ㄱ']) */
|
|
@@ -418,6 +435,8 @@ function matchesPatternAtom(ch, atom) {
|
|
|
418
435
|
* 0건이 된다(조용히 무관 처리하지 않는다 — 제약이 무시된 채 결과가 나오는 오해를 막기 위해).
|
|
419
436
|
*/
|
|
420
437
|
export function matchesSyllablePattern(word, pattern) {
|
|
438
|
+
word = word.normalize('NFC');
|
|
439
|
+
pattern = pattern.normalize('NFC'); // 조각 한글(NFD) 붙여넣기
|
|
421
440
|
const atoms = pattern.split(',').flatMap(chunk => parseChunkToAtoms(chunk.trim()));
|
|
422
441
|
if (atoms.length === 0)
|
|
423
442
|
return true;
|
|
@@ -497,7 +516,8 @@ function isSearchAtomChar(ch) {
|
|
|
497
516
|
* @returns 유효하면 `{ ok: true }`, 아니면 `{ ok: false }`(호출부가 안내 문구를 표시).
|
|
498
517
|
*/
|
|
499
518
|
export function validateSyllableSearchInput(input) {
|
|
500
|
-
|
|
519
|
+
// 조각 한글(NFD) 붙여넣기가 '해석할 수 없는 글자' 로 입력 오류 안내를 띄웠다
|
|
520
|
+
const trimmed = input.normalize('NFC').trim();
|
|
501
521
|
if (!trimmed)
|
|
502
522
|
return { ok: true };
|
|
503
523
|
for (const raw of trimmed.split(',')) {
|
|
@@ -643,16 +663,22 @@ export function countPhonemeDiff(a, b) {
|
|
|
643
663
|
* @public
|
|
644
664
|
*/
|
|
645
665
|
export function searchAndRankWords(items, query) {
|
|
646
|
-
const q = query.trim();
|
|
666
|
+
const q = query.normalize('NFC').trim(); // 조각 한글(NFD) 붙여넣기
|
|
647
667
|
if (!q || !/[^\s,_]/.test(q))
|
|
648
668
|
return items;
|
|
649
669
|
if (isSyllablePattern(q))
|
|
650
670
|
return items.filter(w => matchesSyllablePattern(w.word, q));
|
|
651
|
-
|
|
671
|
+
// 검색어 준비는 한 번 — 낱말마다 다시 하면 키 한 번에 2.3만 번 펼치고 분류했다
|
|
672
|
+
const m = makeWordQueryMatcher(q);
|
|
652
673
|
const headLen = [...q].length;
|
|
653
|
-
const startsWith = (s) => !!s &&
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
674
|
+
const startsWith = (s) => !!s && m(s.normalize('NFC').slice(0, headLen));
|
|
675
|
+
// 순위는 두 갈래로 나눠 잇는다(안정 정렬과 같은 순서)
|
|
676
|
+
const 앞 = [];
|
|
677
|
+
const 뒤 = [];
|
|
678
|
+
for (const w of items) {
|
|
679
|
+
if (!(m(w.word) || m(w.pronunciation ?? '')))
|
|
680
|
+
continue;
|
|
681
|
+
(startsWith(w.word) || startsWith(w.pronunciation ?? '') ? 앞 : 뒤).push(w);
|
|
682
|
+
}
|
|
683
|
+
return 앞.concat(뒤);
|
|
658
684
|
}
|
|
@@ -484,9 +484,26 @@ preferAsTarget) {
|
|
|
484
484
|
for (let p = 0; p < target.syls.length; p++) {
|
|
485
485
|
const key = makeKey(target.syls, p);
|
|
486
486
|
const candidates = groups.get(key) ?? [];
|
|
487
|
+
const ts = target.syls[p];
|
|
487
488
|
for (const candidate of candidates) {
|
|
488
489
|
if (candidate.entry.word === target.entry.word)
|
|
489
490
|
continue;
|
|
491
|
+
/* 같은 묶음은 p 자리 밖이 모두 같다 — p 자리에서 정확히 한 성분만 달라야 최소대립쌍이다. 1음절 낱말은 전부 한 묶음이라
|
|
492
|
+
묶음이 커서, 쌍 문자열·발음 분해(analyzeMinimalPair) 전에 먼저 거른다(음소를 고를 때마다 110–320ms 걸렸다). */
|
|
493
|
+
{
|
|
494
|
+
const cs = candidate.syls[p];
|
|
495
|
+
let d = 0;
|
|
496
|
+
if (ts.초성 !== cs.초성)
|
|
497
|
+
d++;
|
|
498
|
+
if (ts.모음 !== cs.모음)
|
|
499
|
+
d++;
|
|
500
|
+
if (ts.종성 !== cs.종성)
|
|
501
|
+
d++;
|
|
502
|
+
if ((ts.활음 ?? '') !== (cs.활음 ?? ''))
|
|
503
|
+
d++;
|
|
504
|
+
if (d !== 1)
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
490
507
|
const dedupKey = target.entry.word < candidate.entry.word
|
|
491
508
|
? `${target.entry.word}|${candidate.entry.word}`
|
|
492
509
|
: `${candidate.entry.word}|${target.entry.word}`;
|
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
/** 글자 둘을 한국어 차례로 견준다 */
|
|
2
2
|
export declare function 한글순(a: string, b: string): number;
|
|
3
|
+
/**
|
|
4
|
+
* 표의 한 칸끼리 견준다 — **숫자는 숫자로, 나머지는 글자 속 숫자까지 수로.**
|
|
5
|
+
*
|
|
6
|
+
* 표의 칸은 무엇이든 올 수 있어서 늘 글자로 견주면 **숫자 칸이 망가진다**
|
|
7
|
+
* (`10` 이 `9` 앞에 선다). 그래서 둘 다 온전한 숫자(수, 또는 숫자만 든 글자)면 숫자로 견주고,
|
|
8
|
+
* 그 밖에는 한국어 차례로 견주되 글자 안의 숫자는 수로 읽는다(`회차 9` 가 `회차 10` 앞).
|
|
9
|
+
*
|
|
10
|
+
* 비어 있는 값(`null`·`undefined`)은 빈 글자로 본다 — 부르는 쪽이 이미
|
|
11
|
+
* `?? ''` 로 채워 넘기던 것과 같은 뜻이다.
|
|
12
|
+
*/
|
|
3
13
|
export declare function 칸순(a: unknown, b: unknown): number;
|
|
@@ -22,16 +22,6 @@ const 콜레이터 = (() => {
|
|
|
22
22
|
export function 한글순(a, b) {
|
|
23
23
|
return 콜레이터().compare(a, b);
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
26
|
-
* 표의 한 칸끼리 견준다 — **숫자는 숫자로, 나머지는 글자로.**
|
|
27
|
-
*
|
|
28
|
-
* 표의 칸은 무엇이든 올 수 있어서 늘 글자로 견주면 **숫자 칸이 망가진다**
|
|
29
|
-
* (`10` 이 `9` 앞에 선다). 그래서 둘 다 진짜 숫자일 때만 숫자로 견주고,
|
|
30
|
-
* 그 밖에는 한국어 차례를 쓴다.
|
|
31
|
-
*
|
|
32
|
-
* 비어 있는 값(`null`·`undefined`)은 빈 글자로 본다 — 부르는 쪽이 이미
|
|
33
|
-
* `?? ''` 로 채워 넘기던 것과 같은 뜻이다.
|
|
34
|
-
*/
|
|
35
25
|
const 숫자글자 = /^-?\d+(\.\d+)?$/;
|
|
36
26
|
/* 칸의 글자 비교는 **안의 숫자도 수로** 본다 — 숫자 칸끼리는 수로, 섞인 칸은 글자로 견주면 '9'<'10'(수)·'10'<'10분'(글자)·
|
|
37
27
|
'10분'<'9'(글자) 로 돌고 돌아, 같은 행을 들어온 차례만 바꿔도 정렬 결과가 달랐다. 수를 수로 읽는 콜레이터는
|
|
@@ -44,6 +34,16 @@ const 칸콜레이터 = (() => {
|
|
|
44
34
|
return 것;
|
|
45
35
|
};
|
|
46
36
|
})();
|
|
37
|
+
/**
|
|
38
|
+
* 표의 한 칸끼리 견준다 — **숫자는 숫자로, 나머지는 글자 속 숫자까지 수로.**
|
|
39
|
+
*
|
|
40
|
+
* 표의 칸은 무엇이든 올 수 있어서 늘 글자로 견주면 **숫자 칸이 망가진다**
|
|
41
|
+
* (`10` 이 `9` 앞에 선다). 그래서 둘 다 온전한 숫자(수, 또는 숫자만 든 글자)면 숫자로 견주고,
|
|
42
|
+
* 그 밖에는 한국어 차례로 견주되 글자 안의 숫자는 수로 읽는다(`회차 9` 가 `회차 10` 앞).
|
|
43
|
+
*
|
|
44
|
+
* 비어 있는 값(`null`·`undefined`)은 빈 글자로 본다 — 부르는 쪽이 이미
|
|
45
|
+
* `?? ''` 로 채워 넘기던 것과 같은 뜻이다.
|
|
46
|
+
*/
|
|
47
47
|
export function 칸순(a, b) {
|
|
48
48
|
/* NaN 은 숫자가 아닌 것으로 — a - b 가 NaN 이면 정렬 비교가 어긋나 줄이 뒤섞였다. 글자로 온 숫자('10'·'9')도
|
|
49
49
|
둘 다 온전한 숫자면 숫자로 견준다(전에는 '10' 이 '9' 앞에 섰다). */
|
|
@@ -51,5 +51,10 @@ export function 칸순(a, b) {
|
|
|
51
51
|
const na = 수(a), nb = 수(b);
|
|
52
52
|
if (na !== null && nb !== null)
|
|
53
53
|
return na - nb;
|
|
54
|
-
|
|
54
|
+
const sa = a == null ? '' : String(a), sb = b == null ? '' : String(b);
|
|
55
|
+
/* 소수점·음수가 든 글자('12.5%'·'-10 dB')는 수로 읽는 콜레이터가 틀린다 — 점 뒤를 따로 떼어 '12.5' 가 '12.25' 뒤에,
|
|
56
|
+
빼기표를 버려 '-5' 가 '-10' 뒤에 섰다. 그런 칸은 한국어 글자 차례로 둔다. 선행 0 도 수로 읽으면 'A007'='A7' 이 되어 글자로. */
|
|
57
|
+
if (/\d\.\d|-\d|(^|\D)0\d/.test(sa) || /\d\.\d|-\d|(^|\D)0\d/.test(sb))
|
|
58
|
+
return 한글순(sa, sb);
|
|
59
|
+
return 칸콜레이터().compare(sa, sb);
|
|
55
60
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ms` 뒤 끊기는 AbortSignal. `AbortSignal.timeout` 은 Safari 16 부터라 iOS/iPadOS 15 이하(치료실 구형 iPad)에서는
|
|
3
|
+
* 없어 fetch 를 부르기도 전에 TypeError 로 거절됐다 — 없으면 AbortController + setTimeout 으로 만든다.
|
|
4
|
+
*/
|
|
5
|
+
export function timeoutSignal(ms) {
|
|
6
|
+
const native = AbortSignal.timeout;
|
|
7
|
+
if (typeof native === 'function')
|
|
8
|
+
return native.call(AbortSignal, ms);
|
|
9
|
+
const c = new AbortController();
|
|
10
|
+
setTimeout(() => c.abort(), ms);
|
|
11
|
+
return c.signal;
|
|
12
|
+
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* @public
|
|
22
22
|
*/
|
|
23
|
+
import { timeoutSignal } from './timeoutSignal';
|
|
23
24
|
const GLOBAL_KEY = '__ihab_core_tts_config__';
|
|
24
25
|
/**
|
|
25
26
|
* TTS URL 리졸버를 전역으로 설정합니다.
|
|
@@ -84,7 +85,7 @@ export async function checkAudioExists(word) {
|
|
|
84
85
|
audioExistsCache.delete(url);
|
|
85
86
|
return false;
|
|
86
87
|
};
|
|
87
|
-
const promise = fetch(url, { method: 'HEAD', signal:
|
|
88
|
+
const promise = fetch(url, { method: 'HEAD', signal: timeoutSignal(8000) })
|
|
88
89
|
.then(res => (res.status >= 500 ? 확인안됨() : res.ok), 확인안됨);
|
|
89
90
|
audioExistsCache.set(url, promise);
|
|
90
91
|
return promise;
|
|
@@ -141,15 +141,17 @@ position) {
|
|
|
141
141
|
answersByPos.set(i, [pw]);
|
|
142
142
|
}
|
|
143
143
|
for (const [pos, answers] of answersByPos) {
|
|
144
|
-
|
|
144
|
+
// 가릴 성분은 대립 자리(pos)의 것뿐 — 낱말·자리·성분마다 'i:part' 문자열을 만들어 Set 에서 찾던 것을 불리언으로(열쇠 모양은 같다)
|
|
145
|
+
const mC = selParts.includes('cho'), mJ = selParts.includes('jung'), mG = selParts.includes('jong');
|
|
145
146
|
// 마스킹 템플릿은 '발음' 분해 기준 (대립 자리 외 발음이 전부 같은 단어끼리 묶음)
|
|
146
147
|
const templateOf = (d) => {
|
|
147
148
|
let key = '';
|
|
148
149
|
for (let i = 0; i < L; i++) {
|
|
149
150
|
const s = d[i];
|
|
150
|
-
|
|
151
|
-
key += (
|
|
152
|
-
|
|
151
|
+
if (i === pos)
|
|
152
|
+
key += (mC ? '*' : s.cho) + '.' + (mJ ? '*' : s.jung) + '.' + (mG ? '*' : s.jong) + '.';
|
|
153
|
+
else
|
|
154
|
+
key += s.cho + '.' + s.jung + '.' + s.jong + '.';
|
|
153
155
|
}
|
|
154
156
|
return key;
|
|
155
157
|
};
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 재료 고르기는 성능이 전부고(캐시·부분 셔플·병렬 이미지 확인), 문제 만들기는 훈련 타입마다
|
|
10
10
|
* 규칙이 다르다. 한쪽을 볼 때 다른 쪽이 끼어들지 않게 갈라 둔다.
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import { getImageExistenceStats, getImageOkWordSet } from './imageConfig';
|
|
13
13
|
import { getWordPool } from './wordPool';
|
|
14
14
|
import { shuffle } from './array';
|
|
15
15
|
import { getPairIndex } from './wordPairIndex';
|
|
@@ -143,11 +143,12 @@ export function filterByCachedImage(words) {
|
|
|
143
143
|
return all;
|
|
144
144
|
}
|
|
145
145
|
// prewarm 완료 → 이미지 확인된 단어만 허용
|
|
146
|
+
// 낱말마다 '${w}_1' 열쇠를 만들어 찾던 것을 캐시 판마다 한 번 만든 집합으로(칩 한 번에 유형 수만큼 돌았다)
|
|
147
|
+
const ok = getImageOkWordSet();
|
|
146
148
|
const result = new Set();
|
|
147
|
-
for (const w of words)
|
|
148
|
-
if (
|
|
149
|
+
for (const w of words)
|
|
150
|
+
if (ok.has(w))
|
|
149
151
|
result.add(w);
|
|
150
|
-
}
|
|
151
152
|
return result;
|
|
152
153
|
}
|
|
153
154
|
// ─── buildUsablePool 캐시 ──────────────────────────────────────────────────
|
|
@@ -440,7 +441,15 @@ async function _buildUsablePool(opts) {
|
|
|
440
441
|
else if (useRandomPool) {
|
|
441
442
|
// 'random' 또는 null(difficulty:false 타입): 음운론적 제약은 없지만,
|
|
442
443
|
// 정답과 오답의 음절수는 같아야 한다(단어 길이만으로 정답을 짐작하지 못하도록) — 본 단어와 다른 단어 + 음절수(글자수) 동일
|
|
443
|
-
|
|
444
|
+
const 쓸수있음 = (other) => other !== w.w && other.length === w.w.length && pronOfWord(other) !== wPron;
|
|
445
|
+
/* 셈(fullCount — 가능 단어 수 안내)은 '오답이 하나라도 있나' 만 본다. 2.2만 후보마다 600개를 끝까지 걸러 칩 한 번에
|
|
446
|
+
300ms 넘게 멈췄다. 셈은 결과 개수만 쓰고(countUsableWords → pool.length) 오답 목록은 쓰지 않는다. */
|
|
447
|
+
if (opts.fullCount) {
|
|
448
|
+
const one = distractorPool.find(쓸수있음);
|
|
449
|
+
usable = one === undefined ? [] : [one];
|
|
450
|
+
}
|
|
451
|
+
else
|
|
452
|
+
usable = distractorPool.filter(쓸수있음);
|
|
444
453
|
}
|
|
445
454
|
else {
|
|
446
455
|
// cho/jung/jong: 발음 기반 pairIndex에서 해당 종류 대립쌍 조회(공용 엔진). 구조 필터는 후처리에서 일괄.
|
|
@@ -39,7 +39,7 @@ export { copyText } from './utils/clipboard';
|
|
|
39
39
|
export { formatPhone, normalizeDate, dateToDb, formatTimeHM, formatCount } from './utils/format';
|
|
40
40
|
export { configureTrainingCategories, getTopicTypes, getTrainingTypes } from './utils/categories';
|
|
41
41
|
export type { TopicType, TopicSubType, TrainingType, TrainingCategoryConfig } from './utils/categories';
|
|
42
|
-
export { formatYmd, formatRelativeTime, getTodayKst, toKstYmd, toKstHm, formatDateKo, formatDateLongKo, formatDatetimeKo, formatDateLocale, formatDateLongLocale } from './utils/date';
|
|
42
|
+
export { formatYmd, formatRelativeTime, getTodayKst, getTodayKstDate, kstStartOfDay, parseLocalYmd, toKstYmd, toKstHm, formatDateKo, formatDateLongKo, formatDatetimeKo, formatDateLocale, formatDateLongLocale } from './utils/date';
|
|
43
43
|
export type { FormatYmdOptions } from './utils/date';
|
|
44
44
|
export { getSavedTheme, applyTheme, initTheme } from './utils/theme';
|
|
45
45
|
export type { ThemeMode } from './utils/theme';
|
|
@@ -27,7 +27,7 @@ export { toStorageName, toBase64UrlSafe, officialSoundPath } from './utils/encod
|
|
|
27
27
|
export { copyText } from './utils/clipboard';
|
|
28
28
|
export { formatPhone, normalizeDate, dateToDb, formatTimeHM, formatCount } from './utils/format';
|
|
29
29
|
export { configureTrainingCategories, getTopicTypes, getTrainingTypes } from './utils/categories';
|
|
30
|
-
export { formatYmd, formatRelativeTime, getTodayKst, toKstYmd, toKstHm, formatDateKo, formatDateLongKo, formatDatetimeKo, formatDateLocale, formatDateLongLocale } from './utils/date';
|
|
30
|
+
export { formatYmd, formatRelativeTime, getTodayKst, getTodayKstDate, kstStartOfDay, parseLocalYmd, toKstYmd, toKstHm, formatDateKo, formatDateLongKo, formatDatetimeKo, formatDateLocale, formatDateLongLocale } from './utils/date';
|
|
31
31
|
export { getSavedTheme, applyTheme, initTheme } from './utils/theme';
|
|
32
32
|
export { getSavedFontScale, applyFontScale, initFontScale } from './utils/fontScale';
|
|
33
33
|
export { classifyPairDifficulty, firstChoseongOf, toChoseong, isAllChoseong, matchesChoseongSearch } from './utils/koreanSyllable';
|