@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
|
@@ -12,35 +12,35 @@ import { FILTER_ALL_VALUE } from '../types/filterPanel';
|
|
|
12
12
|
* 고른 것이 없는데 '전체' 보기도 없는 절(말할 것이 없다).
|
|
13
13
|
*/
|
|
14
14
|
export function buildFilterSummary(sections, selected, allLabel) {
|
|
15
|
-
const
|
|
15
|
+
const parts = [];
|
|
16
16
|
for (const section of sections ?? []) {
|
|
17
17
|
if (section.type === 'note' || section.type === 'input')
|
|
18
18
|
continue;
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
19
|
+
const name = section.displayLabel ?? section.label;
|
|
20
|
+
const value = valueText(section, selected?.[section.label], allLabel);
|
|
21
|
+
if (value === null)
|
|
22
22
|
continue;
|
|
23
|
-
|
|
23
|
+
parts.push(`${name} ${value}`);
|
|
24
24
|
}
|
|
25
|
-
return
|
|
25
|
+
return parts.join(' · ');
|
|
26
26
|
}
|
|
27
27
|
/** 한 절이 무엇으로 정해져 있는지 — 말할 것이 없으면 `null` */
|
|
28
|
-
function
|
|
29
|
-
const
|
|
28
|
+
function valueText(section, sel, allLabel) {
|
|
29
|
+
const picked = Array.isArray(sel) ? sel : sel === undefined ? [] : [sel];
|
|
30
30
|
/* '전체' 는 두 꼴로 온다 — 아무것도 안 고른 것과 '전체' 보기를 고른 것. 같은 말로 읽는다. */
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
31
|
+
const actual = picked.filter(v => v !== FILTER_ALL_VALUE);
|
|
32
|
+
if (actual.length === 0) {
|
|
33
33
|
return section.allOption ? (section.allLabel ?? allLabel) : null;
|
|
34
34
|
}
|
|
35
|
-
return
|
|
35
|
+
return actual.map(v => optionText(section, v)).join(', ');
|
|
36
36
|
}
|
|
37
37
|
/** 고른 값 하나를 사람이 읽는 말로 — 보기 목록에 없으면 값 그대로 */
|
|
38
|
-
function
|
|
39
|
-
const
|
|
38
|
+
function optionText(section, value) {
|
|
39
|
+
const options = [
|
|
40
40
|
...(section.items ?? []),
|
|
41
41
|
...(section.groups ?? []).flat(),
|
|
42
42
|
];
|
|
43
|
-
for (const item of
|
|
43
|
+
for (const item of options) {
|
|
44
44
|
if (item == null)
|
|
45
45
|
continue;
|
|
46
46
|
if (typeof item === 'string') {
|
|
@@ -67,10 +67,10 @@ export function formatPhone(raw) {
|
|
|
67
67
|
* **사람에게 거는 번호** — 휴대폰 · 지역번호(02·0NN) · 050X 안심번호 · 0303 착신전환.
|
|
68
68
|
* 새 대역은 여기에만 더하면 아래 둘이 함께 받는다.
|
|
69
69
|
*/
|
|
70
|
-
const
|
|
70
|
+
const PERSONAL_PHONE = String.raw `(?:0\d{1,2}|050\d|0303)-\d{3,4}-\d{4}`;
|
|
71
71
|
/** **대표번호**(1588 등) — 거는 곳이 사람이 아니라 기관이다 */
|
|
72
|
-
const
|
|
73
|
-
export const PHONE_REGEX = new RegExp(`^(?:${
|
|
72
|
+
const SERVICE_PHONE = String.raw `1[5-9]\d{2}-\d{4}`;
|
|
73
|
+
export const PHONE_REGEX = new RegExp(`^(?:${PERSONAL_PHONE}|${SERVICE_PHONE})$`);
|
|
74
74
|
/**
|
|
75
75
|
* **대표번호를 뺀 것만** 받는 자 — `PHONE_REGEX` 에서 대표번호 갈래를 뺀 것이다.
|
|
76
76
|
*
|
|
@@ -81,7 +81,7 @@ export const PHONE_REGEX = new RegExp(`^(?:${개인번호}|${대표번호})$`);
|
|
|
81
81
|
* 않는 대역이 하나 더 생기는 날 **두 앱이 조용히 그것을 받게 된다.** 뜻대로 적어 둔다.
|
|
82
82
|
* @public
|
|
83
83
|
*/
|
|
84
|
-
export const PERSONAL_PHONE_REGEX = new RegExp(`^${
|
|
84
|
+
export const PERSONAL_PHONE_REGEX = new RegExp(`^${PERSONAL_PHONE}$`);
|
|
85
85
|
/** 연락처가 `formatPhone` 이 끝까지 채워 낸 모양인가 — 규칙은 `PHONE_REGEX`. @public */
|
|
86
86
|
export function isValidPhone(phone) {
|
|
87
87
|
return PHONE_REGEX.test(phone);
|
|
@@ -106,7 +106,7 @@ export async function loadTrainingData() {
|
|
|
106
106
|
}
|
|
107
107
|
/* 다 읽은 뒤에 한꺼번에 넣는다 — 도중에 형식 오류로 멈추면 이미 넣은 것이 남은 채 재시도가 또 넣어, 부를 때마다 풀에
|
|
108
108
|
중복이 쌓였다(하루 선택이 중복 문항을 뽑고 기기마다 세트가 달라짐). */
|
|
109
|
-
const
|
|
109
|
+
const received = { noise: [], short: [], long: [], order: [] };
|
|
110
110
|
try {
|
|
111
111
|
for (const key of Object.keys(map || {})) {
|
|
112
112
|
const groups = map[key] || [];
|
|
@@ -121,16 +121,16 @@ export async function loadTrainingData() {
|
|
|
121
121
|
// Preserve original group structure from allTrainingData.json
|
|
122
122
|
const groupObj = Object.assign({}, gr, { training_type: t, category_0: cat0, category_1: cat1 });
|
|
123
123
|
if (t === 'LISTEN_IN_NOISE') {
|
|
124
|
-
|
|
124
|
+
received.noise.push(groupObj);
|
|
125
125
|
}
|
|
126
126
|
else if (t === 'SHORT_STORY') {
|
|
127
|
-
|
|
127
|
+
received.short.push(groupObj);
|
|
128
128
|
}
|
|
129
129
|
else if (t === 'LONG_STORY') {
|
|
130
|
-
|
|
130
|
+
received.long.push(groupObj);
|
|
131
131
|
}
|
|
132
132
|
else if (t === 'ORDERING_SENTENCE') {
|
|
133
|
-
|
|
133
|
+
received.order.push(groupObj);
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -148,10 +148,10 @@ export async function loadTrainingData() {
|
|
|
148
148
|
catch { /* SSR 등 */ }
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
|
-
NOISE_DATAS.push(
|
|
152
|
-
SHORT_STORIES.push(
|
|
153
|
-
LONG_STORIES.push(
|
|
154
|
-
ORDER_SETS.push(
|
|
151
|
+
NOISE_DATAS.push(...received.noise);
|
|
152
|
+
SHORT_STORIES.push(...received.short);
|
|
153
|
+
LONG_STORIES.push(...received.long);
|
|
154
|
+
ORDER_SETS.push(...received.order);
|
|
155
155
|
// 다 채운 뒤 _loaded 를 먼저 세운다 — 'updated' 를 받은 쪽이 isTrainingDataLoaded() 로 확인하면 아직 false 였다
|
|
156
156
|
_loaded = true;
|
|
157
157
|
try {
|
|
@@ -13,11 +13,11 @@ const LS_KEY = 'word_image_existence_map_v1';
|
|
|
13
13
|
let memMap = null;
|
|
14
14
|
/* 캐시가 바뀔 때마다 오르는 판 번호 — 통계·'있음' 낱말 집합을 판마다 한 번만 센다(단어훈련 칩 한 번에 유형 수만큼
|
|
15
15
|
2.3만 칸을 훑었다). 캐시를 바꾸는 곳(확인 결과 담기·일괄 주입·비우기)은 모두 판을 올린다. */
|
|
16
|
-
let
|
|
17
|
-
let
|
|
18
|
-
let
|
|
19
|
-
let
|
|
20
|
-
let
|
|
16
|
+
let cacheVersion = 0;
|
|
17
|
+
let statsVersion = -1;
|
|
18
|
+
let statsValue = null;
|
|
19
|
+
let okVersion = -1;
|
|
20
|
+
let okSet = new Set();
|
|
21
21
|
const inflight = new Map(); // 중복 fetch 방지
|
|
22
22
|
function ensureLoaded() {
|
|
23
23
|
if (memMap)
|
|
@@ -80,7 +80,7 @@ export async function checkImageExists(word, homonum) {
|
|
|
80
80
|
if (ok === null)
|
|
81
81
|
return false;
|
|
82
82
|
map[k] = ok;
|
|
83
|
-
|
|
83
|
+
cacheVersion++;
|
|
84
84
|
scheduleSave();
|
|
85
85
|
return ok;
|
|
86
86
|
});
|
|
@@ -143,24 +143,24 @@ export async function prewarmImageCache(words, options = {}) {
|
|
|
143
143
|
*/
|
|
144
144
|
export function getImageExistenceStats() {
|
|
145
145
|
const map = ensureLoaded();
|
|
146
|
-
if (
|
|
147
|
-
return
|
|
146
|
+
if (statsVersion === cacheVersion)
|
|
147
|
+
return statsValue ? { ...statsValue } : null;
|
|
148
148
|
const values = Object.values(map);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return
|
|
149
|
+
statsValue = values.length === 0 ? null : { total: values.length, exists: values.filter(Boolean).length };
|
|
150
|
+
statsVersion = cacheVersion;
|
|
151
|
+
return statsValue ? { ...statsValue } : null;
|
|
152
152
|
}
|
|
153
153
|
/** @internal 동음이의 번호 없이(=1) '이미지 있음' 으로 확인된 낱말 집합 — 캐시 판마다 한 번 만든다. 돌려받은 집합을 고치지 말 것. */
|
|
154
154
|
export function getImageOkWordSet() {
|
|
155
155
|
const map = ensureLoaded();
|
|
156
|
-
if (
|
|
157
|
-
return
|
|
156
|
+
if (okVersion === cacheVersion)
|
|
157
|
+
return okSet;
|
|
158
158
|
const s = new Set();
|
|
159
159
|
for (const k in map)
|
|
160
160
|
if (map[k] === true && k.endsWith('_1'))
|
|
161
161
|
s.add(k.slice(0, -2));
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
okSet = s;
|
|
163
|
+
okVersion = cacheVersion;
|
|
164
164
|
return s;
|
|
165
165
|
}
|
|
166
166
|
/**
|
|
@@ -171,7 +171,7 @@ export function getImageOkWordSet() {
|
|
|
171
171
|
export function bulkSetImageExistence(entries) {
|
|
172
172
|
const map = ensureLoaded();
|
|
173
173
|
Object.assign(map, entries);
|
|
174
|
-
|
|
174
|
+
cacheVersion++;
|
|
175
175
|
try {
|
|
176
176
|
if (typeof localStorage !== 'undefined') {
|
|
177
177
|
localStorage.setItem(LS_KEY, JSON.stringify(map));
|
|
@@ -182,7 +182,7 @@ export function bulkSetImageExistence(entries) {
|
|
|
182
182
|
/** @public 캐시 초기화 (이미지 데이터 변경 시 호출) */
|
|
183
183
|
export function clearImageExistenceCache() {
|
|
184
184
|
memMap = {};
|
|
185
|
-
|
|
185
|
+
cacheVersion++;
|
|
186
186
|
try {
|
|
187
187
|
if (typeof localStorage !== 'undefined')
|
|
188
188
|
localStorage.removeItem(LS_KEY);
|
|
@@ -241,10 +241,10 @@ export function wordContainsJungseong(word, jamo) {
|
|
|
241
241
|
}
|
|
242
242
|
/** 음절의 종성이 고른 자모 하나와 맞는지 — 두 모드의 단일 출처. */
|
|
243
243
|
function jongMatchesJamo(jong, jamo, mode) {
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
244
|
+
const glyph = JONGSEONG[jong];
|
|
245
|
+
if (glyph === jamo)
|
|
246
246
|
return true;
|
|
247
|
-
return mode === 'phoneme' && finalToPhoneme(
|
|
247
|
+
return mode === 'phoneme' && finalToPhoneme(glyph) === finalToPhoneme(jamo);
|
|
248
248
|
}
|
|
249
249
|
/** @public 단어가 특정 자모를 종성(coda)으로 포함하는지.
|
|
250
250
|
* `jongMatch` 로 **글자 그대로**(기본) · **받침 소리**(`'phoneme'`)를 고른다 — `JongMatch` 설명을 본다. */
|
|
@@ -373,41 +373,41 @@ export function wordHasConsonantAt(word, parts, position,
|
|
|
373
373
|
jongMatch = 'glyph') {
|
|
374
374
|
if (!word)
|
|
375
375
|
return false;
|
|
376
|
-
const
|
|
377
|
-
if (
|
|
376
|
+
const syllables = [...word].map(parseSyllable).filter((s) => !!s);
|
|
377
|
+
if (syllables.length === 0)
|
|
378
378
|
return false;
|
|
379
|
-
const
|
|
379
|
+
const last = syllables.length - 1;
|
|
380
380
|
if (position === 'head') {
|
|
381
381
|
/* 어두는 **첫 음절의 초성**뿐이다. `ㅇ` 은 음가가 없어 자리를 안 가진다 —
|
|
382
382
|
`아기` 의 어두초성은 **없다**(`cho: ['ㅇ']` 로 물으면 거짓). */
|
|
383
383
|
if (!parts.cho)
|
|
384
384
|
return false;
|
|
385
|
-
const c = CHOSEONG[
|
|
385
|
+
const c = CHOSEONG[syllables[0].cho];
|
|
386
386
|
return c !== 'ㅇ' && parts.cho.includes(c);
|
|
387
387
|
}
|
|
388
388
|
if (position === 'tail') {
|
|
389
389
|
/* 어말은 **마지막 받침**뿐이다. 받침이 없으면 자리가 없다. */
|
|
390
390
|
if (parts.jongNone)
|
|
391
|
-
return
|
|
391
|
+
return syllables[last].jong === 0;
|
|
392
392
|
if (!parts.jong)
|
|
393
393
|
return false;
|
|
394
|
-
return syllableJongMatches(
|
|
394
|
+
return syllableJongMatches(syllables[last], parts.jong, jongMatch);
|
|
395
395
|
}
|
|
396
396
|
/* 어중 — 첫 초성과 마지막 종성을 **뺀** 나머지 자음 전부. 2음절 `선물` 이면
|
|
397
397
|
ㄴ(0번 종성) · ㅁ(1번 초성) 둘이 어중이다. */
|
|
398
|
-
const
|
|
398
|
+
const onsetMatches = !parts.cho || syllables.some((s, i) => {
|
|
399
399
|
if (i === 0)
|
|
400
400
|
return false; // 첫 초성은 어두다
|
|
401
401
|
const c = CHOSEONG[s.cho];
|
|
402
402
|
return c !== 'ㅇ' && parts.cho.includes(c); // 음가 없는 ㅇ 은 자리를 안 가진다
|
|
403
403
|
});
|
|
404
|
-
const
|
|
405
|
-
?
|
|
406
|
-
: !parts.jong ||
|
|
404
|
+
const codaMatches = parts.jongNone
|
|
405
|
+
? syllables.some((s, i) => i !== last && s.jong === 0)
|
|
406
|
+
: !parts.jong || syllables.some((s, i) => i !== last && syllableJongMatches(s, parts.jong, jongMatch));
|
|
407
407
|
/* 둘 다 줬으면 **둘 다** 있어야 한다(AND) — 하나만 줬으면 그쪽만 참이면 된다 */
|
|
408
408
|
if (!parts.cho && !parts.jong && !parts.jongNone)
|
|
409
409
|
return false;
|
|
410
|
-
return
|
|
410
|
+
return onsetMatches && codaMatches;
|
|
411
411
|
}
|
|
412
412
|
/**
|
|
413
413
|
* @public 문자열이 음절 패턴 DSL 문법인지 — 쉼표(,) 포함 여부로 판별한다.
|
|
@@ -749,12 +749,12 @@ export function searchAndRankWords(items, query) {
|
|
|
749
749
|
const headLen = [...q].length;
|
|
750
750
|
const startsWith = (s) => !!s && m(s.normalize('NFC').slice(0, headLen));
|
|
751
751
|
// 순위는 두 갈래로 나눠 잇는다(안정 정렬과 같은 순서)
|
|
752
|
-
const
|
|
753
|
-
const
|
|
752
|
+
const before = [];
|
|
753
|
+
const after = [];
|
|
754
754
|
for (const w of items) {
|
|
755
755
|
if (!(m(w.word) || m(w.pronunciation ?? '')))
|
|
756
756
|
continue;
|
|
757
|
-
(startsWith(w.word) || startsWith(w.pronunciation ?? '') ?
|
|
757
|
+
(startsWith(w.word) || startsWith(w.pronunciation ?? '') ? before : after).push(w);
|
|
758
758
|
}
|
|
759
|
-
return
|
|
759
|
+
return before.concat(after);
|
|
760
760
|
}
|
|
@@ -17,8 +17,8 @@ export function scrollNavTargetToTop(scrollContainer, from = 'Nav') {
|
|
|
17
17
|
el.scrollTop = 0;
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
20
|
+
const doc = document.scrollingElement ?? document.documentElement;
|
|
21
|
+
if (doc && doc.scrollHeight <= doc.clientHeight) {
|
|
22
22
|
devWarnOnce('nav-scroll-container', `[${from}] 본문을 맨 위로 되돌릴 통을 못 찾았다(기본 ${DEFAULT_SCROLL_SELECTOR}) — 창도 구를 수 없어 아무 일도 하지 않는다. scrollContainer 로 통을 알려 준다.`);
|
|
23
23
|
}
|
|
24
24
|
window.scrollTo({ top: 0 });
|
|
@@ -15,4 +15,11 @@ export declare const VOWEL_TO_PHONEME: Record<string, {
|
|
|
15
15
|
export declare function vowelGlidesDiffer(a: string, b: string): boolean;
|
|
16
16
|
/** 종성 글자 → 음소(대표음) 변환. 매핑에 없으면 원본 유지. */
|
|
17
17
|
export declare function finalToPhoneme(jong: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* 고른 글자를 음소 꼴로 — **열쇠는 코드로 낸다**(`onset`·`vowel`·`coda`·`glide`).
|
|
20
|
+
*
|
|
21
|
+
* **들어오는 것은 둘 다 받는다** — 저장된 단어표의 고른값은 옛 한글 열쇠(`{ 초성: 'ㄴ' }`)일
|
|
22
|
+
* 수 있다(2026-10-13 코드로 옮기며, 저장된 표를 밀지 않으려고 읽는 쪽을 너그럽게 뒀다).
|
|
23
|
+
* 값(자모·`'ㄴ'`)은 한국어 글자 그 자체라 그대로 둔다 — 바꿀 것은 **열쇠와 빔 값**이다.
|
|
24
|
+
*/
|
|
18
25
|
export declare function convertToPhoneme(sel: Record<string, string>): Record<string, string>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toPhonemeCodes, PHONEME_NONE } from './phonemeSectionCodes';
|
|
1
2
|
export const VALID_GLIDES = {
|
|
2
3
|
ㅏ: ['j', 'w'], ㅓ: ['j', 'w'], ㅗ: ['j'], ㅜ: ['j'],
|
|
3
4
|
'ㅔ/ㅐ': ['j', 'w'], ㅣ: ['w', 'ɰ'], ㅡ: [],
|
|
@@ -44,21 +45,29 @@ export function vowelGlidesDiffer(a, b) {
|
|
|
44
45
|
export function finalToPhoneme(jong) {
|
|
45
46
|
return JONGSEONG_TO_PHONEME[jong] ?? jong;
|
|
46
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* 고른 글자를 음소 꼴로 — **열쇠는 코드로 낸다**(`onset`·`vowel`·`coda`·`glide`).
|
|
50
|
+
*
|
|
51
|
+
* **들어오는 것은 둘 다 받는다** — 저장된 단어표의 고른값은 옛 한글 열쇠(`{ 초성: 'ㄴ' }`)일
|
|
52
|
+
* 수 있다(2026-10-13 코드로 옮기며, 저장된 표를 밀지 않으려고 읽는 쪽을 너그럽게 뒀다).
|
|
53
|
+
* 값(자모·`'ㄴ'`)은 한국어 글자 그 자체라 그대로 둔다 — 바꿀 것은 **열쇠와 빔 값**이다.
|
|
54
|
+
*/
|
|
47
55
|
export function convertToPhoneme(sel) {
|
|
56
|
+
const coded = toPhonemeCodes(sel);
|
|
48
57
|
const result = {};
|
|
49
|
-
if (
|
|
50
|
-
result
|
|
51
|
-
if (
|
|
52
|
-
const m = VOWEL_TO_PHONEME[
|
|
53
|
-
// 활음을 항상 명시한다(
|
|
54
|
-
//
|
|
58
|
+
if (coded.onset)
|
|
59
|
+
result.onset = coded.onset;
|
|
60
|
+
if (coded.vowel) {
|
|
61
|
+
const m = VOWEL_TO_PHONEME[coded.vowel];
|
|
62
|
+
// 활음을 항상 명시한다(빔 포함). 단어 분해(decomposePhonemicSyllable)는 활음 없는 모음에도
|
|
63
|
+
// glide: 빔을 넣으므로, 선택 변환도 대칭으로 맞춰야 활음 대비(예: ㅐ↔ㅙ = 빔↔w)가 매칭된다.
|
|
55
64
|
// (안 넣으면 ㅐ/ㅔ↔ㅚ/ㅙ 대립이 모음(nucleus)만 같고 대비 섹션이 없어 0건이 됨.)
|
|
56
65
|
if (m) {
|
|
57
|
-
result
|
|
58
|
-
result
|
|
66
|
+
result.vowel = m.nucleus;
|
|
67
|
+
result.glide = m.glide ?? PHONEME_NONE;
|
|
59
68
|
}
|
|
60
69
|
}
|
|
61
|
-
if (
|
|
62
|
-
result
|
|
70
|
+
if (coded.coda)
|
|
71
|
+
result.coda = JONGSEONG_TO_PHONEME[coded.coda] ?? coded.coda;
|
|
63
72
|
return result;
|
|
64
73
|
}
|
|
@@ -50,8 +50,8 @@ export function phoneticLabel(t, f) {
|
|
|
50
50
|
return f.glide ? `${v} · ${GLIDE_LABEL[f.glide](t)}` : v;
|
|
51
51
|
}
|
|
52
52
|
/** 기본값만 돌려주는 t — 옛 `tooltip`(한국어 고정)을 같은 표에서 만들어 두 글이 갈라지지 않게 한다. */
|
|
53
|
-
const
|
|
54
|
-
const item = (jamos, phonetic) => ({ jamos, phonetic, tooltip: phoneticLabel(
|
|
53
|
+
const koTranslate = (_key, { defaultValue }) => defaultValue;
|
|
54
|
+
const item = (jamos, phonetic) => ({ jamos, phonetic, tooltip: phoneticLabel(koTranslate, phonetic) });
|
|
55
55
|
/** 자음 행: 같은 조음 위치를 공유하는 단일 자모 버튼들. */
|
|
56
56
|
const consonantRow = (place, jamos) => jamos.map(j => item([j], { place }));
|
|
57
57
|
/** 모음/병합 아이템: `chars`가 여러 글자면 동음 병합 버튼(예: 'ㅔㅐ' → ['ㅔ','ㅐ']). */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { WordEntry, PairFilterKeys, SearchPairsDebugReport } from './phonemePairSearchTypes';
|
|
2
2
|
import type { WordSearchResultItem } from '../types/wordSearch';
|
|
3
|
+
export { toPhonemeCodes, PHONEME_NONE, type PhonemeSectionCode } from './phonemeSectionCodes';
|
|
3
4
|
import type { WordEntry } from './phonemePairSearchTypes';
|
|
4
5
|
import type { PairFilterKeys } from './phonemePairSearchTypes';
|
|
5
6
|
/** applyFilters/matchesFilters가 공통으로 판정하는 필드 — WordEntry와 WordSearchResultWord
|