@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,15 +8,20 @@ function decompose(char) {
|
|
|
8
8
|
if (code < 0 || code > 11171)
|
|
9
9
|
return null;
|
|
10
10
|
return {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
onset: CHO[Math.floor(code / 588)],
|
|
12
|
+
vowel: JUNG[Math.floor((code % 588) / 28)],
|
|
13
|
+
coda: JONG[code % 28] || '',
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
16
|
+
/* 음절 자리 코드는 **아래 모듈**에 있다 — `phonemeConvert` 도 함께 쓰는데, 여기 두면
|
|
17
|
+
`phonemePairSearch ↔ phonemeConvert` 돌림 import 가 된다(한 번 그렇게 해 시험 338 꾸러미가
|
|
18
|
+
무너졌다, 2026-10-13). */
|
|
19
|
+
import { toPhonemeCodes, toLegacyPhonemeKey, isPhonemeSectionCode, PHONEME_SECTION_CODES, PHONEME_NONE } from './phonemeSectionCodes';
|
|
20
|
+
export { toPhonemeCodes, PHONEME_NONE } from './phonemeSectionCodes';
|
|
21
|
+
const SECTION_KEYS = PHONEME_SECTION_CODES;
|
|
22
|
+
const NONE = PHONEME_NONE;
|
|
23
|
+
const isSectionKey = isPhonemeSectionCode;
|
|
24
|
+
const toCodes = toPhonemeCodes;
|
|
20
25
|
// 발음형 분해: 모음을 활음 + 핵모음으로 분리
|
|
21
26
|
const VOWEL_GLIDE = {
|
|
22
27
|
ㅑ: 'j', ㅒ: 'j', ㅕ: 'j', ㅖ: 'j', ㅛ: 'j', ㅠ: 'j',
|
|
@@ -38,10 +43,10 @@ function decomposePhonemicSyllable(char) {
|
|
|
38
43
|
const rawJung = JUNG[jungIdx];
|
|
39
44
|
const rawJong = JONG[jongIdx];
|
|
40
45
|
return {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
onset: rawCho === 'ㅇ' ? NONE : rawCho,
|
|
47
|
+
glide: VOWEL_GLIDE[rawJung] ?? NONE,
|
|
48
|
+
vowel: VOWEL_NUCLEUS[rawJung] ?? rawJung,
|
|
49
|
+
coda: rawJong || NONE,
|
|
45
50
|
};
|
|
46
51
|
}
|
|
47
52
|
function analyzeMinimalPair(word1, word2, phonemic = false) {
|
|
@@ -94,8 +99,8 @@ function diffHasEmptyPair(section, vals) {
|
|
|
94
99
|
// '활음' 섹션의 '없음'은 비움으로 보지 않는다 — 발음 모드에서 사용자가 활음 자리를 **직접 골라** 찾는 대립(ㅐ↔ㅙ = 없음↔w)이라
|
|
95
100
|
// 그 검색 결과를 이 필터가 통째로 지우면 안 된다.
|
|
96
101
|
const isEmpty = (v) => v === '' ||
|
|
97
|
-
(section === '
|
|
98
|
-
(section === '
|
|
102
|
+
(section === 'coda' && v === NONE) ||
|
|
103
|
+
(section === 'onset' && (v === 'ㅇ' || v === NONE));
|
|
99
104
|
if (isEmpty(vals[0]) || isEmpty(vals[1]))
|
|
100
105
|
return true;
|
|
101
106
|
// 모음(글자 모드) 활음 구조 차이: 두 모음의 반모음(활음)이 다르면 음절 '구조' 차이로 본다.
|
|
@@ -104,7 +109,7 @@ function diffHasEmptyPair(section, vals) {
|
|
|
104
109
|
// 발음 모드는 모음=핵모음이라 둘 다 활음 없음으로 읽혀 영향 없음. 활음 대립은
|
|
105
110
|
// 별도 '활음' 섹션에서 처리되므로 여기(모음)에서 걸러도 활음 지정 대립과 충돌하지 않는다.
|
|
106
111
|
// 단어훈련 런타임(wordContrast.hasStructureContrast)도 **같은 함수**로 판정한다.
|
|
107
|
-
if (section === '
|
|
112
|
+
if (section === 'vowel' && vowelGlidesDiffer(vals[0], vals[1]))
|
|
108
113
|
return true;
|
|
109
114
|
return false;
|
|
110
115
|
}
|
|
@@ -125,13 +130,13 @@ const DEFAULT_PAIR_FILTER_KEYS = {
|
|
|
125
130
|
posMap: POS_GROUP_MAP_KO,
|
|
126
131
|
typeMap: WORD_TYPE_MAP_KO,
|
|
127
132
|
};
|
|
128
|
-
const
|
|
129
|
-
function
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
return
|
|
133
|
-
const
|
|
134
|
-
const
|
|
133
|
+
const collected = new WeakMap();
|
|
134
|
+
function collect(filterSelected, keys) {
|
|
135
|
+
const hit = collected.get(filterSelected);
|
|
136
|
+
if (hit && hit.keys === keys)
|
|
137
|
+
return hit.value;
|
|
138
|
+
const value = { grades: [], syls: [], poses: [], types: [] };
|
|
139
|
+
const bySection = { grade: value.grades, syllables: value.syls, pos: value.poses, wordType: value.types };
|
|
135
140
|
for (const [label, raw] of Object.entries(filterSelected ?? {})) {
|
|
136
141
|
/* 섹션은 넘긴 keys 의 라벨이면 그것, 아니면 어느 언어·옛 라벨·코드든 읽는다(wordAttributeSectionOf) */
|
|
137
142
|
const s = label === keys.difficulty ? 'grade' : label === keys.syllables ? 'syllables'
|
|
@@ -140,11 +145,11 @@ function 모으기(filterSelected, keys) {
|
|
|
140
145
|
if (!s)
|
|
141
146
|
continue;
|
|
142
147
|
for (const v of Array.isArray(raw) ? raw : typeof raw === 'string' ? [raw] : [])
|
|
143
|
-
if (
|
|
144
|
-
|
|
148
|
+
if (!bySection[s].includes(v))
|
|
149
|
+
bySection[s].push(v);
|
|
145
150
|
}
|
|
146
|
-
|
|
147
|
-
return
|
|
151
|
+
collected.set(filterSelected, { keys, value });
|
|
152
|
+
return value;
|
|
148
153
|
}
|
|
149
154
|
/**
|
|
150
155
|
* 난이도/음절수/품사/유형 필터를 단어 하나에 대해 판정(의미범주 제외 — WordEntry 전용이라 applyFilters에서 별도 처리).
|
|
@@ -154,7 +159,7 @@ function 모으기(filterSelected, keys) {
|
|
|
154
159
|
* **풀리고** 품사·어종 값은 posMap 에 없어 **모든 단어가 떨어졌다**.
|
|
155
160
|
*/
|
|
156
161
|
export function matchesFilters(word, filterSelected, keys = DEFAULT_PAIR_FILTER_KEYS) {
|
|
157
|
-
const { grades, syls, poses, types } =
|
|
162
|
+
const { grades, syls, poses, types } = collect(filterSelected, keys);
|
|
158
163
|
if (grades.length) {
|
|
159
164
|
/* **`없음` 은 값이 아니라 빔이다**((205), 2026-10-12) — 거르는 결정은 `matchesGradeCodes` **하나**다
|
|
160
165
|
(단어훈련 풀도 같은 것을 부른다). 여기서는 라벨을 코드로만 바꾼다.
|
|
@@ -278,9 +283,15 @@ function searchPairsCore(selectedList, words, debug, enforcePosition, phonemic =
|
|
|
278
283
|
const uniqueWordsCount = new Set(words.map(w => w.word)).size;
|
|
279
284
|
const bySection = {};
|
|
280
285
|
const bySectionValueOrder = {};
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
286
|
+
/* **들어오는 자리에서 한 번 옮긴다** — 저장된 단어표의 `phonemeSelected` 는 옛 한글 열쇠일
|
|
287
|
+
수 있다(`{ 초성: 'ㄴ' }`). 안쪽은 코드만 보므로 여기서 가른다. */
|
|
288
|
+
const codedList = selectedList.map(toCodes);
|
|
289
|
+
for (let i = 0; i < codedList.length; i++) {
|
|
290
|
+
for (const [section, value] of Object.entries(codedList[i])) {
|
|
291
|
+
/* **`!value` 로 거르지 않는다.** `{ 종성: '' }` 은 "받침 없음을 골랐다" 는 뜻이고,
|
|
292
|
+
옮기며 그 줄을 더했다가 `받침 있음↔없음` 검색이 빈손이 됐다(2026-10-13, 시험이 잡았다).
|
|
293
|
+
*옮기기에 거르는 줄을 끼워 넣으면 뜻이 바뀐다.* */
|
|
294
|
+
if (!isSectionKey(section) || value === undefined)
|
|
284
295
|
continue;
|
|
285
296
|
if (!bySection[section])
|
|
286
297
|
bySection[section] = new Set();
|
|
@@ -315,16 +326,16 @@ function searchPairsCore(selectedList, words, debug, enforcePosition, phonemic =
|
|
|
315
326
|
const targetKeys = new Set(sectionEntries.map(([s]) => s));
|
|
316
327
|
const contrastSections = new Set(sectionEntries.filter(([, vals]) => vals.size >= 2).map(([s]) => s));
|
|
317
328
|
const encodeAll = phonemic
|
|
318
|
-
? (s) => s
|
|
319
|
-
: (s) => s
|
|
329
|
+
? (s) => s.onset + (s.glide ?? '') + s.vowel + s.coda
|
|
330
|
+
: (s) => s.onset + s.vowel + s.coda;
|
|
320
331
|
const encodeWild = phonemic
|
|
321
|
-
? (s) => (targetKeys.has('
|
|
322
|
-
(targetKeys.has('
|
|
323
|
-
(targetKeys.has('
|
|
324
|
-
(targetKeys.has('
|
|
325
|
-
: (s) => (targetKeys.has('
|
|
326
|
-
(targetKeys.has('
|
|
327
|
-
(targetKeys.has('
|
|
332
|
+
? (s) => (targetKeys.has('onset') ? '*' : s.onset) +
|
|
333
|
+
(targetKeys.has('glide') ? '*' : (s.glide ?? '')) +
|
|
334
|
+
(targetKeys.has('vowel') ? '*' : s.vowel) +
|
|
335
|
+
(targetKeys.has('coda') ? '*' : s.coda)
|
|
336
|
+
: (s) => (targetKeys.has('onset') ? '*' : s.onset) +
|
|
337
|
+
(targetKeys.has('vowel') ? '*' : s.vowel) +
|
|
338
|
+
(targetKeys.has('coda') ? '*' : s.coda);
|
|
328
339
|
const makeKeyForPosition = (syllables, diffPos) => syllables.map((s, i) => (i === diffPos ? encodeWild(s) : encodeAll(s))).join('|');
|
|
329
340
|
const gradeOrder = (g) => {
|
|
330
341
|
const parsed = Number.parseInt(g ?? '', 10);
|
|
@@ -335,9 +346,9 @@ function searchPairsCore(selectedList, words, debug, enforcePosition, phonemic =
|
|
|
335
346
|
한 묶음의 낱말은 모두 같은 자리를 비운 것이다. 분해한 음절도 함께 든다(견줄 때마다 다시 분해하지 않게). */
|
|
336
347
|
const groups = new Map();
|
|
337
348
|
/* 표기가 같아도 뜻마다 칸이 하나다 — 판정은 대표로 한 번만 하고 결과에서 뜻을 펼친다 */
|
|
338
|
-
const {
|
|
349
|
+
const { heads, senses } = groupBySense(words);
|
|
339
350
|
const seenWords = new Set();
|
|
340
|
-
for (const entry of
|
|
351
|
+
for (const entry of heads) {
|
|
341
352
|
seenWords.add(entry.word);
|
|
342
353
|
const decomposeFn = phonemic ? decomposePhonemicSyllable : decompose;
|
|
343
354
|
const syllables = [...cleanPron(entry.pronunciation)].map(decomposeFn);
|
|
@@ -346,25 +357,25 @@ function searchPairsCore(selectedList, words, debug, enforcePosition, phonemic =
|
|
|
346
357
|
const syls = syllables;
|
|
347
358
|
for (let p = 0; p < syls.length; p++) {
|
|
348
359
|
const key = makeKeyForPosition(syls, p);
|
|
349
|
-
let
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
groups.set(key,
|
|
360
|
+
let group = groups.get(key);
|
|
361
|
+
if (!group) {
|
|
362
|
+
group = { emptyAt: p, members: [] };
|
|
363
|
+
groups.set(key, group);
|
|
353
364
|
}
|
|
354
|
-
|
|
365
|
+
group.members.push({ entry, syls });
|
|
355
366
|
}
|
|
356
367
|
}
|
|
357
368
|
const results = [];
|
|
358
369
|
const rejectCounts = createRejectCounter();
|
|
359
370
|
const rejectSamples = [];
|
|
360
371
|
let comparedPairs = 0;
|
|
361
|
-
for (const {
|
|
362
|
-
if (
|
|
372
|
+
for (const { emptyAt, members } of groups.values()) {
|
|
373
|
+
if (members.length < 2)
|
|
363
374
|
continue;
|
|
364
|
-
for (let i = 0; i <
|
|
365
|
-
for (let j = i + 1; j <
|
|
366
|
-
const w1 =
|
|
367
|
-
const w2 =
|
|
375
|
+
for (let i = 0; i < members.length; i++) {
|
|
376
|
+
for (let j = i + 1; j < members.length; j++) {
|
|
377
|
+
const w1 = members[i].entry;
|
|
378
|
+
const w2 = members[j].entry;
|
|
368
379
|
/* 같은 표기가 발음만 다르게 두 번 들어온 자료(김밥[김밥]·김밥[김빱])는 짝으로 세지 않는다 —
|
|
369
380
|
칸 둘의 이름이 같은 '김밥/김밥' 쌍이 된다. 견준 쌍을 다 기억하던 전에는 이것이
|
|
370
381
|
덤으로 막히고 있었다(같은 표기면 열쇠가 하나라 한 번만 지나갔다). */
|
|
@@ -383,15 +394,15 @@ function searchPairsCore(selectedList, words, debug, enforcePosition, phonemic =
|
|
|
383
394
|
남은 것은 '한 군데만 다른가' 를 쌍 문자열·발음 분해 전에 여기서 재는 일이다 —
|
|
384
395
|
analyzeMinimalPair 와 같은 잣대(SECTION_KEYS)를 그 한 자리에만 쓴다. 포함 검색
|
|
385
396
|
(searchWordsByPhonemes)이 전부터 하던 것과 같은 걸름이다. */
|
|
386
|
-
const s1 =
|
|
387
|
-
const s2 =
|
|
388
|
-
let
|
|
397
|
+
const s1 = members[i].syls[emptyAt];
|
|
398
|
+
const s2 = members[j].syls[emptyAt];
|
|
399
|
+
let diffCount = 0;
|
|
389
400
|
for (const sec of SECTION_KEYS)
|
|
390
401
|
if (s1[sec] !== s2[sec])
|
|
391
|
-
|
|
402
|
+
diffCount += 1;
|
|
392
403
|
comparedPairs += 1;
|
|
393
|
-
if (
|
|
394
|
-
const reason =
|
|
404
|
+
if (diffCount !== 1) {
|
|
405
|
+
const reason = diffCount === 0 ? 'no_difference' : 'multiple_differences';
|
|
395
406
|
rejectCounts[reason] += 1;
|
|
396
407
|
if (debug && rejectSamples.length < 20)
|
|
397
408
|
rejectSamples.push({ id: `${w1.word}|${w2.word}`, reason });
|
|
@@ -421,13 +432,15 @@ function searchPairsCore(selectedList, words, debug, enforcePosition, phonemic =
|
|
|
421
432
|
const sDiffVals = analysis.diff.diffs[sDiffSection];
|
|
422
433
|
const sDiffHasEmpty = diffHasEmptyPair(sDiffSection, sDiffVals);
|
|
423
434
|
// 뜻마다 한 칸 — '눈'(신체)·'눈'(날씨) 가 각각 '문' 과 쌍을 이룬다
|
|
424
|
-
for (const f of
|
|
425
|
-
for (const sec of
|
|
435
|
+
for (const f of senses.get(first) ?? [first]) {
|
|
436
|
+
for (const sec of senses.get(second) ?? [second]) {
|
|
426
437
|
results.push({
|
|
427
|
-
id: `${
|
|
428
|
-
|
|
438
|
+
id: `${wordKey(f)}|${wordKey(sec)}`,
|
|
439
|
+
/* 옛 이름을 내보내고 코드를 나란히 — 소비자가 옛 값으로 묶음을 가른다 */
|
|
440
|
+
diffSection: toLegacyPhonemeKey(sDiffSection),
|
|
441
|
+
diffSectionCode: sDiffSection,
|
|
429
442
|
diffHasEmpty: sDiffHasEmpty,
|
|
430
|
-
words: [
|
|
443
|
+
words: [wordCells(f), wordCells(sec)],
|
|
431
444
|
});
|
|
432
445
|
}
|
|
433
446
|
}
|
|
@@ -469,38 +482,38 @@ export function searchPairs(selectedList, words, enforcePosition, phonemic = fal
|
|
|
469
482
|
// 슬롯 i → 단어의 i번째 음절에 조건 매칭 (순서 보장)
|
|
470
483
|
/** 표기가 같아도 뜻이 다르면 다른 칸 — 칸에 붙는 그림이 뜻마다 다르다(사용자 결정 2026-09-16).
|
|
471
484
|
* 동음이의 번호가 없는 자료는 예전처럼 표기뿐이다. */
|
|
472
|
-
function
|
|
485
|
+
function wordKey(entry) {
|
|
473
486
|
return entry.homonum != null ? `${entry.word}#${entry.homonum}` : entry.word;
|
|
474
487
|
}
|
|
475
488
|
/** 표기·발음이 같은 뜻들을 한 대표로 묶는다 — 최소대립쌍 판정은 표기와 발음만 보므로 뜻마다 다시 셀 까닭이 없다.
|
|
476
489
|
* (뜻을 모두 후보로 넣으면 묶음이 커져 23,000 낱말에서 검색이 2–4배 느려졌다.) 결과를 만들 때 뜻마다 칸을 펼친다. */
|
|
477
490
|
/** 결과 칸 하나 */
|
|
478
|
-
function
|
|
491
|
+
function wordCells(e) {
|
|
479
492
|
return {
|
|
480
493
|
label: e.word, subLabel: e.pronunciation, colloquialLabel: e.colloquialPronunciation, homonum: e.homonum,
|
|
481
494
|
grade: e.grade, syllables: [...e.word].length, pos: e.pos, wordType: e.wordType, isCustom: e.isCustom,
|
|
482
495
|
};
|
|
483
496
|
}
|
|
484
|
-
function
|
|
485
|
-
const
|
|
486
|
-
const
|
|
487
|
-
const
|
|
497
|
+
function groupBySense(words) {
|
|
498
|
+
const byKey = new Map();
|
|
499
|
+
const senses = new Map();
|
|
500
|
+
const heads = [];
|
|
488
501
|
for (const entry of words) {
|
|
489
|
-
const
|
|
490
|
-
const
|
|
491
|
-
if (
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
502
|
+
const key = `${entry.word}\u0000${cleanPron(entry.pronunciation)}`;
|
|
503
|
+
const head = byKey.get(key);
|
|
504
|
+
if (head === undefined) {
|
|
505
|
+
byKey.set(key, entry);
|
|
506
|
+
heads.push(entry);
|
|
507
|
+
senses.set(entry, [entry]);
|
|
495
508
|
continue;
|
|
496
509
|
}
|
|
497
|
-
const
|
|
510
|
+
const list = senses.get(head);
|
|
498
511
|
// 같은 번호가 두 번 오면(자료 중복) 한 번만
|
|
499
|
-
if (
|
|
512
|
+
if (list.some(e => wordKey(e) === wordKey(entry)))
|
|
500
513
|
continue;
|
|
501
|
-
|
|
514
|
+
list.push(entry);
|
|
502
515
|
}
|
|
503
|
-
return {
|
|
516
|
+
return { heads, senses };
|
|
504
517
|
}
|
|
505
518
|
function wordContainsAllPhonemes(syllables, requirements) {
|
|
506
519
|
for (let i = 0; i < requirements.length; i++) {
|
|
@@ -534,18 +547,20 @@ export function searchWordsByPhonemes(requiredPhonemes, words, ordered = true, p
|
|
|
534
547
|
* 이 조건을 만족하는 쪽을 우선한다(예: "검색 단어만" 필터 통과 여부). 지정하지
|
|
535
548
|
* 않으면 기존 동작(먼저 발견된 방향을 그대로 채택)과 동일하다. */
|
|
536
549
|
preferAsTarget) {
|
|
537
|
-
|
|
550
|
+
/* 들어오는 자리에서 코드로 — 옛 한글 열쇠로 저장된 표도 그대로 읽힌다 */
|
|
551
|
+
const coded = requiredPhonemes.map(toCodes);
|
|
552
|
+
const reqs = coded.filter(r => Object.keys(r).some(k => isSectionKey(k)));
|
|
538
553
|
if (reqs.length === 0)
|
|
539
554
|
return [];
|
|
540
555
|
const encodeAll = phonemic
|
|
541
|
-
? (s) => s
|
|
542
|
-
: (s) => s
|
|
556
|
+
? (s) => s.onset + (s.glide ?? '') + s.vowel + s.coda
|
|
557
|
+
: (s) => s.onset + s.vowel + s.coda;
|
|
543
558
|
const makeKey = (syls, pos) => syls.map((s, i) => (i === pos ? '*' : encodeAll(s))).join('|');
|
|
544
559
|
const decomposeFn = phonemic ? decomposePhonemicSyllable : decompose;
|
|
545
560
|
const allWords = [];
|
|
546
|
-
const {
|
|
561
|
+
const { heads, senses } = groupBySense(words);
|
|
547
562
|
const seenWords = new Set();
|
|
548
|
-
for (const entry of
|
|
563
|
+
for (const entry of heads) {
|
|
549
564
|
seenWords.add(entry.word);
|
|
550
565
|
const syls = [...cleanPron(entry.pronunciation)].map(decomposeFn);
|
|
551
566
|
if (syls.some(s => s === null))
|
|
@@ -563,7 +578,7 @@ preferAsTarget) {
|
|
|
563
578
|
/* 일치(ordered) 는 조건의 자리가 곧 음절 자리다 — 빈 슬롯을 걸러 낸 reqs 를 쓰면 [{}, {초성:ㄴ}] 의 ㄴ 이 첫 음절에 걸렸다.
|
|
564
579
|
wordContainsAllPhonemes 는 빈 슬롯을 스스로 건너뛴다. */
|
|
565
580
|
return ordered
|
|
566
|
-
? wordContainsAllPhonemes(filterSyls,
|
|
581
|
+
? wordContainsAllPhonemes(filterSyls, coded)
|
|
567
582
|
: wordContainsAllPhonemesUnordered(filterSyls, reqs);
|
|
568
583
|
});
|
|
569
584
|
const groups = new Map();
|
|
@@ -575,7 +590,7 @@ preferAsTarget) {
|
|
|
575
590
|
groups.get(key).push(w);
|
|
576
591
|
}
|
|
577
592
|
}
|
|
578
|
-
const
|
|
593
|
+
const pairs = [];
|
|
579
594
|
// dedupKey → 쌍들 배열 내 인덱스. 같은 두 단어가 서로 minimal pair이면서
|
|
580
595
|
// 둘 다 독립적으로 targetWords에 속할 수 있어(예: 다른 음절에 요구 음소가 하나 더 있는 경우),
|
|
581
596
|
// 어느 쪽을 target(1번/words[0])으로 채택할지가 순회 순서에 좌우될 수 있다.
|
|
@@ -594,13 +609,13 @@ preferAsTarget) {
|
|
|
594
609
|
{
|
|
595
610
|
const cs = candidate.syls[p];
|
|
596
611
|
let d = 0;
|
|
597
|
-
if (ts
|
|
612
|
+
if (ts.onset !== cs.onset)
|
|
598
613
|
d++;
|
|
599
|
-
if (ts
|
|
614
|
+
if (ts.vowel !== cs.vowel)
|
|
600
615
|
d++;
|
|
601
|
-
if (ts
|
|
616
|
+
if (ts.coda !== cs.coda)
|
|
602
617
|
d++;
|
|
603
|
-
if ((ts
|
|
618
|
+
if ((ts.glide ?? '') !== (cs.glide ?? ''))
|
|
604
619
|
d++;
|
|
605
620
|
if (d !== 1)
|
|
606
621
|
continue;
|
|
@@ -612,7 +627,7 @@ preferAsTarget) {
|
|
|
612
627
|
if (existingIdx !== undefined) {
|
|
613
628
|
if (!preferAsTarget)
|
|
614
629
|
continue;
|
|
615
|
-
if (preferAsTarget(
|
|
630
|
+
if (preferAsTarget(wordCells(pairs[existingIdx].target)))
|
|
616
631
|
continue;
|
|
617
632
|
if (!preferAsTarget({ grade: target.entry.grade, syllables: [...target.entry.word].length, pos: target.entry.pos, wordType: target.entry.wordType }))
|
|
618
633
|
continue;
|
|
@@ -635,12 +650,12 @@ preferAsTarget) {
|
|
|
635
650
|
if (tv !== cv)
|
|
636
651
|
diffValues = [tv, cv];
|
|
637
652
|
}
|
|
638
|
-
const
|
|
653
|
+
const matches = (req) => {
|
|
639
654
|
const reqVal = req?.[diffSection];
|
|
640
655
|
return reqVal !== undefined && (diffValues[0] === reqVal || diffValues[1] === reqVal);
|
|
641
656
|
};
|
|
642
657
|
// 일치(ordered): "첫번째 음절에서 초성 ㄱ 이 대립" — 대립한 자리의 조건이어야 한다(다른 음절에서 대립한 쌍이 섞였다)
|
|
643
|
-
const isRelevant = ordered ?
|
|
658
|
+
const isRelevant = ordered ? matches(coded[analysis.diff.idx]) : reqs.some(matches);
|
|
644
659
|
if (!isRelevant)
|
|
645
660
|
continue;
|
|
646
661
|
}
|
|
@@ -649,26 +664,27 @@ preferAsTarget) {
|
|
|
649
664
|
const diffHasEmpty = diffHasEmptyPair(diffSection, diffVals);
|
|
650
665
|
const item = { target: target.entry, candidate: candidate.entry, diffSection, diffHasEmpty };
|
|
651
666
|
if (existingIdx !== undefined) {
|
|
652
|
-
|
|
667
|
+
pairs[existingIdx] = item;
|
|
653
668
|
}
|
|
654
669
|
else {
|
|
655
|
-
seenPairs.set(dedupKey,
|
|
656
|
-
|
|
670
|
+
seenPairs.set(dedupKey, pairs.length);
|
|
671
|
+
pairs.push(item);
|
|
657
672
|
}
|
|
658
673
|
}
|
|
659
674
|
}
|
|
660
675
|
}
|
|
661
676
|
/* 뜻마다 한 칸 — 기준·짝의 뜻을 모두 펼친다('눈'(신체)·'눈'(날씨) 가 각각 '문' 과 쌍). id 는 정렬 꼴에 번호까지. */
|
|
662
677
|
const results = [];
|
|
663
|
-
for (const
|
|
664
|
-
for (const t of
|
|
665
|
-
for (const c of
|
|
666
|
-
const kt =
|
|
678
|
+
for (const item of pairs) {
|
|
679
|
+
for (const t of senses.get(item.target) ?? [item.target]) {
|
|
680
|
+
for (const c of senses.get(item.candidate) ?? [item.candidate]) {
|
|
681
|
+
const kt = wordKey(t), kc = wordKey(c);
|
|
667
682
|
results.push({
|
|
668
683
|
id: kt < kc ? `${kt}|${kc}` : `${kc}|${kt}`,
|
|
669
|
-
diffSection:
|
|
670
|
-
|
|
671
|
-
|
|
684
|
+
diffSection: toLegacyPhonemeKey(item.diffSection),
|
|
685
|
+
diffSectionCode: item.diffSection,
|
|
686
|
+
diffHasEmpty: item.diffHasEmpty,
|
|
687
|
+
words: [wordCells(t), wordCells(c)],
|
|
672
688
|
});
|
|
673
689
|
}
|
|
674
690
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **음절 자리의 코드** — `onset`·`vowel`·`coda`·`glide`, 그리고 빔(`none`).
|
|
3
|
+
*
|
|
4
|
+
* ## 왜 제 모듈인가
|
|
5
|
+
*
|
|
6
|
+
* 처음에는 이 가름을 `phonemePairSearch` 에 두고 `phonemeConvert` 가 들여왔다 — 그런데
|
|
7
|
+
* `phonemePairSearch` 도 `phonemeConvert`(`VOWEL_TO_PHONEME`)를 들여온다. **돌림 import** 가
|
|
8
|
+
* 되어 시험 **338 꾸러미**가 한꺼번에 무너졌다(2026-10-13). 두 쪽이 함께 쓰는 것은 **어느
|
|
9
|
+
* 한쪽이 아니라 그 아래**에 둔다.
|
|
10
|
+
*
|
|
11
|
+
* ## 왜 코드인가
|
|
12
|
+
*
|
|
13
|
+
* 전에는 열쇠가 한글(`'초성'`)이었고 그것이 **저장된다** — 단어표의
|
|
14
|
+
* `searchConfig.phonemeSelected` 가 그 꼴이다. 저장되는 값이 표시 언어에 묶이면 말을 고치려
|
|
15
|
+
* 해도 자료를 밀어야 한다(사장님 지시 2026-10-13 "동작에 한글이 끼지 말라").
|
|
16
|
+
*
|
|
17
|
+
* **값을 바꾸지 않고 읽는 쪽을 너그럽게 했다** — 표준의 *lenient reader · strict writer*.
|
|
18
|
+
* 저장된 표를 밀지 않아도 되고 소비자 코드도 안 바뀐다. 안쪽 로직에서만 한글이 사라진다.
|
|
19
|
+
*
|
|
20
|
+
* 어휘는 **훈련 필터가 이미 쓰던 것**과 같게 뒀다(`useWordTrainingFilter` 의 `onset`·`vowel`·
|
|
21
|
+
* `coda`) — 같은 뜻에 어휘가 둘이던 것을 모았다.
|
|
22
|
+
*
|
|
23
|
+
* **값(자모 `'ㄴ'`)은 그대로다** — 그것은 한국어 글자 그 자체이고 바꿀 것은 열쇠와 빔 값이다.
|
|
24
|
+
*/
|
|
25
|
+
/** 음절 자리 코드 @public */
|
|
26
|
+
export type PhonemeSectionCode = 'onset' | 'vowel' | 'coda' | 'glide';
|
|
27
|
+
/** @public */
|
|
28
|
+
export declare const PHONEME_SECTION_CODES: PhonemeSectionCode[];
|
|
29
|
+
/** 자리가 비었음 — 초성 ㅇ · 받침 없음 · 활음 없음을 한 값으로 적는다 @public */
|
|
30
|
+
export declare const PHONEME_NONE = "none";
|
|
31
|
+
/** @public */
|
|
32
|
+
export declare function isPhonemeSectionCode(value: string): value is PhonemeSectionCode;
|
|
33
|
+
/**
|
|
34
|
+
* **들어오는 고른값 하나를 코드 꼴로.** 옛 한글 열쇠·옛 빔 값도 받는다.
|
|
35
|
+
*
|
|
36
|
+
* 공개 함수(`searchPairs`·`searchWordsByPhonemes`·`convertToPhoneme`)가 받는
|
|
37
|
+
* `Record<string, string>` 은 **저장된 검색 구성에서 온다** — 그 꼴을 바꾸면 이미 저장된
|
|
38
|
+
* 단어표가 깨진다. 그래서 들어오는 자리에서 한 번 옮기고, 안쪽은 코드만 본다.
|
|
39
|
+
*
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
42
|
+
export declare function toPhonemeCodes(sel: Record<string, string>): Partial<Record<PhonemeSectionCode, string>>;
|
|
43
|
+
/**
|
|
44
|
+
* 코드를 **옛 열쇠 글자로** — 밖으로 나가는 `diffSection` 이 그 꼴이다.
|
|
45
|
+
*
|
|
46
|
+
* 소비자가 그 값으로 묶음을 가른다(말귀 `WordPairsPage` 의 `GROUP_ORDER`·`GROUP_LABEL`).
|
|
47
|
+
* 혼자 코드로 바꾸면 **그쪽 묶음이 조용히 빈다** — 그래서 옛 이름을 내보내고, 코드는
|
|
48
|
+
* `diffSectionCode` 로 **나란히** 둔다(2026-10-13). 소비자가 옮긴 뒤 옛 쪽을 걷는다.
|
|
49
|
+
*
|
|
50
|
+
* @public
|
|
51
|
+
*/
|
|
52
|
+
export declare function toLegacyPhonemeKey(code: PhonemeSectionCode): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **음절 자리의 코드** — `onset`·`vowel`·`coda`·`glide`, 그리고 빔(`none`).
|
|
3
|
+
*
|
|
4
|
+
* ## 왜 제 모듈인가
|
|
5
|
+
*
|
|
6
|
+
* 처음에는 이 가름을 `phonemePairSearch` 에 두고 `phonemeConvert` 가 들여왔다 — 그런데
|
|
7
|
+
* `phonemePairSearch` 도 `phonemeConvert`(`VOWEL_TO_PHONEME`)를 들여온다. **돌림 import** 가
|
|
8
|
+
* 되어 시험 **338 꾸러미**가 한꺼번에 무너졌다(2026-10-13). 두 쪽이 함께 쓰는 것은 **어느
|
|
9
|
+
* 한쪽이 아니라 그 아래**에 둔다.
|
|
10
|
+
*
|
|
11
|
+
* ## 왜 코드인가
|
|
12
|
+
*
|
|
13
|
+
* 전에는 열쇠가 한글(`'초성'`)이었고 그것이 **저장된다** — 단어표의
|
|
14
|
+
* `searchConfig.phonemeSelected` 가 그 꼴이다. 저장되는 값이 표시 언어에 묶이면 말을 고치려
|
|
15
|
+
* 해도 자료를 밀어야 한다(사장님 지시 2026-10-13 "동작에 한글이 끼지 말라").
|
|
16
|
+
*
|
|
17
|
+
* **값을 바꾸지 않고 읽는 쪽을 너그럽게 했다** — 표준의 *lenient reader · strict writer*.
|
|
18
|
+
* 저장된 표를 밀지 않아도 되고 소비자 코드도 안 바뀐다. 안쪽 로직에서만 한글이 사라진다.
|
|
19
|
+
*
|
|
20
|
+
* 어휘는 **훈련 필터가 이미 쓰던 것**과 같게 뒀다(`useWordTrainingFilter` 의 `onset`·`vowel`·
|
|
21
|
+
* `coda`) — 같은 뜻에 어휘가 둘이던 것을 모았다.
|
|
22
|
+
*
|
|
23
|
+
* **값(자모 `'ㄴ'`)은 그대로다** — 그것은 한국어 글자 그 자체이고 바꿀 것은 열쇠와 빔 값이다.
|
|
24
|
+
*/
|
|
25
|
+
/** @public */
|
|
26
|
+
export const PHONEME_SECTION_CODES = ['onset', 'vowel', 'coda', 'glide'];
|
|
27
|
+
/** 자리가 비었음 — 초성 ㅇ · 받침 없음 · 활음 없음을 한 값으로 적는다 @public */
|
|
28
|
+
export const PHONEME_NONE = 'none';
|
|
29
|
+
/** 저장된 옛 열쇠 → 코드. 옛 표를 읽을 때만 쓰는 다리다(새로 쓰는 것은 코드뿐) */
|
|
30
|
+
const LEGACY_SECTION_KEY = {
|
|
31
|
+
초성: 'onset', 모음: 'vowel', 종성: 'coda', 활음: 'glide',
|
|
32
|
+
};
|
|
33
|
+
/** 저장된 옛 '빔' 값 */
|
|
34
|
+
const LEGACY_NONE = '없음';
|
|
35
|
+
/** @public */
|
|
36
|
+
export function isPhonemeSectionCode(value) {
|
|
37
|
+
return PHONEME_SECTION_CODES.includes(value);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* **들어오는 고른값 하나를 코드 꼴로.** 옛 한글 열쇠·옛 빔 값도 받는다.
|
|
41
|
+
*
|
|
42
|
+
* 공개 함수(`searchPairs`·`searchWordsByPhonemes`·`convertToPhoneme`)가 받는
|
|
43
|
+
* `Record<string, string>` 은 **저장된 검색 구성에서 온다** — 그 꼴을 바꾸면 이미 저장된
|
|
44
|
+
* 단어표가 깨진다. 그래서 들어오는 자리에서 한 번 옮기고, 안쪽은 코드만 본다.
|
|
45
|
+
*
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
export function toPhonemeCodes(sel) {
|
|
49
|
+
const out = {};
|
|
50
|
+
for (const [k, v] of Object.entries(sel)) {
|
|
51
|
+
const key = isPhonemeSectionCode(k) ? k : LEGACY_SECTION_KEY[k];
|
|
52
|
+
/* **빈 값은 버리지 않는다.** `{ 종성: '' }` 은 "받침 없음을 골랐다" 는 **뜻 있는 고름**이다 —
|
|
53
|
+
한 번 버렸다가 `받침 있음↔없음` 대립 검색이 빈손이 됐다(2026-10-13, 시험이 잡았다).
|
|
54
|
+
거르는 것은 **열쇠를 못 알아볼 때**뿐이다. */
|
|
55
|
+
if (!key)
|
|
56
|
+
continue;
|
|
57
|
+
out[key] = v === LEGACY_NONE ? PHONEME_NONE : v;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/** 코드 → 저장된 옛 열쇠. 밖으로 내보내는 값이 옛 꼴이어야 할 때만 쓴다 */
|
|
62
|
+
const CODE_TO_LEGACY = {
|
|
63
|
+
onset: '초성', vowel: '모음', coda: '종성', glide: '활음',
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* 코드를 **옛 열쇠 글자로** — 밖으로 나가는 `diffSection` 이 그 꼴이다.
|
|
67
|
+
*
|
|
68
|
+
* 소비자가 그 값으로 묶음을 가른다(말귀 `WordPairsPage` 의 `GROUP_ORDER`·`GROUP_LABEL`).
|
|
69
|
+
* 혼자 코드로 바꾸면 **그쪽 묶음이 조용히 빈다** — 그래서 옛 이름을 내보내고, 코드는
|
|
70
|
+
* `diffSectionCode` 로 **나란히** 둔다(2026-10-13). 소비자가 옮긴 뒤 옛 쪽을 걷는다.
|
|
71
|
+
*
|
|
72
|
+
* @public
|
|
73
|
+
*/
|
|
74
|
+
export function toLegacyPhonemeKey(code) {
|
|
75
|
+
return CODE_TO_LEGACY[code];
|
|
76
|
+
}
|
|
@@ -26,19 +26,19 @@
|
|
|
26
26
|
* @public
|
|
27
27
|
*/
|
|
28
28
|
export function prefetchChunk(...loads) {
|
|
29
|
-
let
|
|
30
|
-
const
|
|
29
|
+
let cancelled = false;
|
|
30
|
+
const load = () => { if (!cancelled)
|
|
31
31
|
for (const load of loads)
|
|
32
32
|
void load().catch(() => { }); };
|
|
33
33
|
const idle = globalThis.requestIdleCallback;
|
|
34
34
|
if (idle) {
|
|
35
|
-
const id = idle(
|
|
35
|
+
const id = idle(load, { timeout: 2000 });
|
|
36
36
|
return () => {
|
|
37
|
-
|
|
37
|
+
cancelled = true;
|
|
38
38
|
const cancel = globalThis.cancelIdleCallback;
|
|
39
39
|
cancel?.(id);
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
-
const id = setTimeout(
|
|
43
|
-
return () => {
|
|
42
|
+
const id = setTimeout(load, 300);
|
|
43
|
+
return () => { cancelled = true; clearTimeout(id); };
|
|
44
44
|
}
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
* 진짜 `<button>` 을 쓸 수 있으면 그쪽이 낫다. 이건 이미 `<div>`·`<li>` 로 짜인
|
|
15
15
|
* 자리를 **모양을 안 바꾸고** 눌리게 만드는 길이다.
|
|
16
16
|
*/
|
|
17
|
-
export function pressable(onPress,
|
|
17
|
+
export function pressable(onPress, options) {
|
|
18
18
|
if (!onPress)
|
|
19
19
|
return {};
|
|
20
20
|
return {
|
|
21
|
-
role:
|
|
21
|
+
role: options?.role ?? 'button',
|
|
22
22
|
tabIndex: 0,
|
|
23
|
-
...(
|
|
23
|
+
...(options?.selected === undefined ? {} : { 'aria-selected': options.selected }),
|
|
24
24
|
onKeyDown: (e) => {
|
|
25
25
|
/* **이 자리 자신에서 난 키만** 받는다. 안에 든 진짜 단추(랜딩 띠의 action 등)에서 누른 Enter 가 올라오면
|
|
26
26
|
전에는 여기서 preventDefault 해 그 단추의 click 을 막고 이 자리의 onPress 를 대신 불렀다. */
|
|
@@ -8,16 +8,16 @@
|
|
|
8
8
|
export function isSafeOpenHref(href) {
|
|
9
9
|
if (typeof href !== 'string')
|
|
10
10
|
return false;
|
|
11
|
-
let
|
|
11
|
+
let stripped = '';
|
|
12
12
|
for (const ch of href) {
|
|
13
13
|
const c = ch.charCodeAt(0);
|
|
14
14
|
if (c <= 0x20 || (c >= 0x7f && c <= 0x9f))
|
|
15
15
|
continue;
|
|
16
|
-
|
|
16
|
+
stripped += ch;
|
|
17
17
|
}
|
|
18
|
-
if (
|
|
18
|
+
if (!stripped)
|
|
19
19
|
return false;
|
|
20
|
-
const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(
|
|
20
|
+
const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(stripped);
|
|
21
21
|
if (!m)
|
|
22
22
|
return true; // 스킴이 없다 — 상대 경로(`/help`·`./a`·`?q`·`#x`)·`//host` 는 지금 문서의 스킴을 따른다
|
|
23
23
|
return ['http', 'https', 'mailto', 'tel'].includes(m[1].toLowerCase());
|