@ihabdevteam/core 0.128.0 → 0.131.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 +100 -0
- package/dist/components.cjs.js +177 -177
- package/dist/components.esm.css +2 -2
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core-tokens.css +1 -1
- package/dist/ihabdevteam-core.cjs.js +182 -182
- package/dist/ihabdevteam-core.css +1 -1
- package/dist/ihabdevteam-core.d.ts +282 -80
- package/dist/packages/core/src/components/AudioMeter.js +29 -15
- package/dist/packages/core/src/components/Avatar.js +6 -2
- package/dist/packages/core/src/components/Badge.d.ts +5 -1
- package/dist/packages/core/src/components/Badge.js +19 -5
- package/dist/packages/core/src/components/BottomNav.css +15 -0
- package/dist/packages/core/src/components/BottomNav.d.ts +6 -0
- package/dist/packages/core/src/components/BottomNav.js +12 -1
- package/dist/packages/core/src/components/BulkBar.d.ts +2 -1
- package/dist/packages/core/src/components/BulkBar.js +9 -2
- package/dist/packages/core/src/components/Calendar.js +1 -1
- package/dist/packages/core/src/components/ColorPicker.d.ts +3 -1
- package/dist/packages/core/src/components/ColorPicker.js +4 -2
- package/dist/packages/core/src/components/DatePicker.d.ts +2 -1
- package/dist/packages/core/src/components/DatePicker.js +9 -4
- package/dist/packages/core/src/components/Effect/EffectPartyPopper.d.ts +9 -0
- package/dist/packages/core/src/components/Effect/EffectPartyPopper.js +30 -25
- package/dist/packages/core/src/components/EmptyState.js +3 -1
- package/dist/packages/core/src/components/ErrorState.js +3 -1
- package/dist/packages/core/src/components/FilterPanel.js +7 -0
- package/dist/packages/core/src/components/Header.d.ts +6 -0
- package/dist/packages/core/src/components/Header.js +27 -1
- package/dist/packages/core/src/components/Icon.css +22 -0
- package/dist/packages/core/src/components/Icon.d.ts +7 -3
- package/dist/packages/core/src/components/Icon.js +7 -1
- package/dist/packages/core/src/components/IconGridMenu.css +4 -2
- package/dist/packages/core/src/components/IconGridMenu.d.ts +1 -1
- package/dist/packages/core/src/components/IconGridMenu.js +6 -3
- package/dist/packages/core/src/components/LandingHeader.js +6 -3
- package/dist/packages/core/src/components/Loader.d.ts +1 -1
- package/dist/packages/core/src/components/Loader.js +7 -2
- package/dist/packages/core/src/components/NoticeBox.d.ts +5 -0
- package/dist/packages/core/src/components/NoticeBox.js +3 -1
- package/dist/packages/core/src/components/Pagination.d.ts +2 -2
- package/dist/packages/core/src/components/Pagination.js +2 -2
- package/dist/packages/core/src/components/PanelRow.js +5 -1
- package/dist/packages/core/src/components/PassCard/PassCard.d.ts +10 -1
- package/dist/packages/core/src/components/PassCard/PassCardBg.css +33 -0
- package/dist/packages/core/src/components/PaymentPlan.d.ts +8 -1
- package/dist/packages/core/src/components/Popup/ConfirmPopup.d.ts +1 -1
- package/dist/packages/core/src/components/Popup/ListPopup.d.ts +8 -1
- package/dist/packages/core/src/components/Popup/ListPopup.js +17 -2
- package/dist/packages/core/src/components/Popup/Popup.css +1 -1
- package/dist/packages/core/src/components/Popup/Popup.d.ts +1 -1
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +3 -2
- package/dist/packages/core/src/components/Popup/WordDataUpdatePopup.d.ts +4 -1
- package/dist/packages/core/src/components/Popup/WordDataUpdatePopup.js +8 -3
- package/dist/packages/core/src/components/Popup/customerDetailUtils.d.ts +7 -2
- package/dist/packages/core/src/components/Popup/customerDetailUtils.js +20 -3
- package/dist/packages/core/src/components/ProgressIndicator.d.ts +2 -5
- package/dist/packages/core/src/components/PromoBanner.js +23 -6
- package/dist/packages/core/src/components/RadioGroup.d.ts +3 -1
- package/dist/packages/core/src/components/RadioGroup.js +2 -2
- package/dist/packages/core/src/components/Report/ReportDocument.d.ts +4 -1
- package/dist/packages/core/src/components/Report/ReportDocument.js +4 -1
- package/dist/packages/core/src/components/Section.d.ts +7 -1
- package/dist/packages/core/src/components/Section.js +6 -2
- package/dist/packages/core/src/components/Table.d.ts +1 -1
- package/dist/packages/core/src/components/TextField.css +2 -1
- package/dist/packages/core/src/components/TextField.js +10 -2
- package/dist/packages/core/src/components/Toggle.js +4 -1
- package/dist/packages/core/src/components/Tooltip/DebugTooltip.d.ts +1 -6
- package/dist/packages/core/src/components/Tooltip/DebugTooltip.js +4 -7
- package/dist/packages/core/src/components/Tooltip/Tooltip.js +11 -1
- package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +3 -3
- package/dist/packages/core/src/components/Training/TrainingBlock.d.ts +8 -3
- package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +10 -11
- package/dist/packages/core/src/components/Training/trainingBlockButtonTypes.d.ts +4 -4
- package/dist/packages/core/src/components/Training/trainingScenario.d.ts +6 -2
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +3 -1
- package/dist/packages/core/src/components/Word/WordTableBlock.js +6 -0
- package/dist/packages/core/src/components/Word/WordTableButton.d.ts +2 -2
- package/dist/packages/core/src/components/Word/WordTableButton.js +1 -1
- package/dist/packages/core/src/components/Word/wordTableBlockTypes.d.ts +1 -1
- package/dist/packages/core/src/components/Word/wordTableScoring.d.ts +1 -4
- package/dist/packages/core/src/components/Word/wordTableScoring.js +1 -4
- package/dist/packages/core/src/components/WordTable.d.ts +1 -1
- package/dist/packages/core/src/components/WordTable.js +3 -1
- package/dist/packages/core/src/components.d.ts +3 -0
- package/dist/packages/core/src/components.js +5 -0
- package/dist/packages/core/src/hooks/useDeviceManagement.js +11 -2
- package/dist/packages/core/src/hooks/useFontScale.js +3 -1
- package/dist/packages/core/src/hooks/useLucideIcon.d.ts +3 -4
- package/dist/packages/core/src/hooks/useLucideIcon.js +3 -4
- package/dist/packages/core/src/hooks/usePasswordChange.js +20 -7
- package/dist/packages/core/src/hooks/useVisitAllPanel.d.ts +7 -0
- package/dist/packages/core/src/hooks/useVisitAllPanel.js +8 -0
- package/dist/packages/core/src/hooks/useWordMeanings.js +3 -0
- package/dist/packages/core/src/hooks/useWordTrainingFilter.d.ts +5 -0
- package/dist/packages/core/src/hooks/useWordTrainingFilter.js +23 -33
- package/dist/packages/core/src/i18n/locales/en.json +34 -6
- package/dist/packages/core/src/i18n/locales/ko.json +34 -6
- package/dist/packages/core/src/tokens.css +2 -1
- package/dist/packages/core/src/utils/CategoryData.d.ts +12 -40
- package/dist/packages/core/src/utils/CategoryData.js +15 -10
- package/dist/packages/core/src/utils/assets.js +7 -3
- package/dist/packages/core/src/utils/clipboard.js +16 -2
- package/dist/packages/core/src/utils/errorMessage.d.ts +2 -1
- package/dist/packages/core/src/utils/errorMessage.js +19 -2
- package/dist/packages/core/src/utils/getTrainingData.d.ts +8 -1
- package/dist/packages/core/src/utils/getTrainingData.js +8 -1
- package/dist/packages/core/src/utils/koreanSyllable.d.ts +15 -12
- package/dist/packages/core/src/utils/koreanSyllable.js +16 -13
- package/dist/packages/core/src/utils/lucideIconSet.d.ts +4 -3
- package/dist/packages/core/src/utils/lucideIconSet.js +4 -3
- package/dist/packages/core/src/utils/ttsResolver.d.ts +5 -0
- package/dist/packages/core/src/utils/wordTrainingGenerators.d.ts +3 -3
- package/dist/packages/core/src/utils/wordTrainingGenerators.js +28 -7
- 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 +4 -2
|
@@ -5,18 +5,23 @@ export const TRAINING_TYPES = [
|
|
|
5
5
|
{ key: "LONG_STORY", label: "긴 이야기 듣기", icon: "message-square-text", desc: "긴 이야기를 듣고 기억하여 다수의 문제를 풀어봅니다." },
|
|
6
6
|
{ key: "ORDERING_SENTENCE", label: "문장 순서화 하기", icon: "align-start-vertical", desc: "여러개의 짧은 문장을 듣고 기억하여 그 순서에 맞게 배열합니다." },
|
|
7
7
|
];
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* 주제·세부 주제 목록(열쇠·이름·아이콘·색). **설명(desc)은 두지 않는다** — 코어·앱 어디에서도 주제 설명을
|
|
10
|
+
* 그리지 않고, 한때 7개 주제에 붙어 있던 '소음 환경 속 문장 이해 훈련' 은 훈련 유형(소음 하 문장 듣기)의
|
|
11
|
+
* 설명을 옮겨 붙인 값이었다(2026-09-14 걷음). 화면에 보이는 설명은 훈련 유형(`TRAINING_TYPES`)의 것이다.
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
9
14
|
export const TOPIC_TYPES = [
|
|
10
|
-
{ key: 'FOOD', label: '음식', icon: 'salad', color: '#FF8A65', subs: [{ key: 'TRKRFD', label: '
|
|
11
|
-
{ key: 'HLTH', label: '건강', icon: 'heart', color: '#66BB6A', subs: [{ key: 'HTETHB', label: '건강한 식습관' }, { key: 'DISILL', label: '병과 질환' }, { key: 'EXCISE', label: '운동' }, { key: 'GEHTKL', label: '
|
|
15
|
+
{ key: 'FOOD', label: '음식', icon: 'salad', color: '#FF8A65', subs: [{ key: 'TRKRFD', label: '한국 전통음식' }, { key: 'MDKRFD', label: '한국 현대음식' }, { key: 'WRLDFD', label: '세계의 음식' }, { key: 'MTRFRT', label: '식재료와 과일' }, { key: 'GEFDKL', label: '음식 상식' }] },
|
|
16
|
+
{ key: 'HLTH', label: '건강', icon: 'heart', color: '#66BB6A', subs: [{ key: 'HTETHB', label: '건강한 식습관' }, { key: 'DISILL', label: '병과 질환' }, { key: 'EXCISE', label: '운동' }, { key: 'GEHTKL', label: '건강상식' }] },
|
|
12
17
|
{ key: 'WETH', label: '날씨', icon: 'sun-medium', color: '#42A5F5', subs: [{ key: 'GESSKL', label: '계절상식' }, { key: 'SNWRAN', label: '눈과 비' }, { key: 'WTHFCT', label: '일기예보' }, { key: 'GEWTKL', label: '날씨상식' }] },
|
|
13
|
-
{ key: 'PRSN', label: '인물', icon: 'user-round',
|
|
14
|
-
{ key: 'KRCT', label: '한국문화', icon: 'bow-arrow',
|
|
15
|
-
{ key: 'STRY', label: '이야기', icon: 'message-circle',
|
|
16
|
-
{ key: 'LOCA', label: '지역', icon: 'map-pin',
|
|
17
|
-
{ key: 'SOAR', label: '사회와 예술', icon: 'landmark',
|
|
18
|
-
{ key: 'NTSC', label: '자연과 과학', icon: 'leaf',
|
|
19
|
-
{ key: 'SPRT', label: '스포츠', icon: 'trophy',
|
|
18
|
+
{ key: 'PRSN', label: '인물', icon: 'user-round', color: '#AB47BC', subs: [{ key: 'JODYPS', label: '조선시대' }, { key: 'MDKRPS', label: '한국 근현대' }, { key: 'THKDPS', label: '삼국시대' }, { key: 'WRLART', label: '세계의 예술가' }, { key: 'WRLPSN', label: '세계의 인물' }] },
|
|
19
|
+
{ key: 'KRCT', label: '한국문화', icon: 'bow-arrow', color: '#EC407A', subs: [{ key: 'KRLANG', label: '우리말' }, { key: 'TRPLCT', label: '전통놀이' }, { key: 'HERITG', label: '문화재' }, { key: 'EVTFES', label: '행사와 축제' }, { key: 'GECTKL', label: '상식' }] },
|
|
20
|
+
{ key: 'STRY', label: '이야기', icon: 'message-circle', color: '#8D6E63', subs: [{ key: 'FRYTAL', label: '동화' }, { key: 'AMZING', label: '신기한 이야기' }, { key: 'DAYSTR', label: '일상 속 이야기' }] },
|
|
21
|
+
{ key: 'LOCA', label: '지역', icon: 'map-pin', color: '#26A69A', subs: [{ key: 'KRLDMK', label: '한국의 명소' }, { key: 'SELNMK', label: '서울의 명소' }, { key: 'WRDTRB', label: '세계여행' }, { key: 'NAVIGT', label: '길 찾기' }] },
|
|
22
|
+
{ key: 'SOAR', label: '사회와 예술', icon: 'landmark', color: '#5C6BC0', subs: [{ key: 'KRSOCI', label: '한국' }, { key: 'ECONEW', label: '경제와 뉴스' }, { key: 'GENART', label: '예술' }, { key: 'GELFKL', label: '생활 상식' }] },
|
|
23
|
+
{ key: 'NTSC', label: '자연과 과학', icon: 'leaf', color: '#9CCC65', subs: [{ key: 'ANIMAL', label: '동물' }, { key: 'PLANTS', label: '식물' }, { key: 'ETHPLT', label: '지구' }, { key: 'TOLELC', label: '도구와 전자제품' }, { key: 'GESCKL', label: '과학상식' }] },
|
|
24
|
+
{ key: 'SPRT', label: '스포츠', icon: 'trophy', color: '#FFCA28', subs: [{ key: 'INVDSP', label: '개인경기' }, { key: 'MARART', label: '격투기' }, { key: 'TEAMSP', label: '단체경기' }, { key: 'FOOTBL', label: '축구' }, { key: 'GESPKL', label: '스포츠 상식' }] },
|
|
20
25
|
];
|
|
21
26
|
/** 훈련 유형 i18n 키 매핑 */
|
|
22
27
|
export const TRAINING_TYPE_I18N_KEY = {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const ABSOLUTE_URL_RE = /^(?:[a-z]+:)?\/\//i;
|
|
2
|
-
|
|
2
|
+
// globalThis 싱글톤: 다른 설정(storage·ttsConfig·imageConfig)과 같은 방식.
|
|
3
|
+
// 모듈 변수로 두면 CJS 묶음이 여럿일 때(utils.cjs 에서 설정, components.cjs 에서 읽기)
|
|
4
|
+
// 묶음마다 한 벌씩 생겨 설정이 안 보였다.
|
|
5
|
+
const ASSET_GLOBAL_KEY = '__ihab_core_asset_base_url__';
|
|
3
6
|
function normalizeAssetBaseUrl(baseUrl) {
|
|
4
7
|
const raw = String(baseUrl || '').trim();
|
|
5
8
|
if (!raw)
|
|
@@ -30,11 +33,12 @@ function normalizeAssetBaseUrl(baseUrl) {
|
|
|
30
33
|
* @public
|
|
31
34
|
*/
|
|
32
35
|
export function setAssetBaseUrl(baseUrl) {
|
|
33
|
-
|
|
36
|
+
;
|
|
37
|
+
globalThis[ASSET_GLOBAL_KEY] = normalizeAssetBaseUrl(baseUrl);
|
|
34
38
|
}
|
|
35
39
|
/** @public */
|
|
36
40
|
export function getAssetBaseUrl() {
|
|
37
|
-
return
|
|
41
|
+
return globalThis[ASSET_GLOBAL_KEY] ?? '';
|
|
38
42
|
}
|
|
39
43
|
/**
|
|
40
44
|
* 에셋 경로를 절대 URL 또는 베이스 URL 기준의 경로로 변환합니다.
|
|
@@ -10,8 +10,12 @@
|
|
|
10
10
|
export async function copyText(text) {
|
|
11
11
|
// 1순위: execCommand (동기 — 제스처 유지, iframe 대응)
|
|
12
12
|
if (typeof document !== 'undefined') {
|
|
13
|
+
// 숨긴 textarea 에 포커스를 옮기므로, 끝나면 원래 자리로 돌려준다 — 안 돌리면 복사 단추를
|
|
14
|
+
// 누른 뒤 키보드 포커스가 body 로 떨어져 스크린리더·Tab 이동이 처음부터 다시 시작된다.
|
|
15
|
+
const 원래포커스 = document.activeElement;
|
|
16
|
+
let ta = null;
|
|
13
17
|
try {
|
|
14
|
-
|
|
18
|
+
ta = document.createElement('textarea');
|
|
15
19
|
ta.value = text;
|
|
16
20
|
ta.setAttribute('readonly', '');
|
|
17
21
|
ta.style.position = 'fixed';
|
|
@@ -24,13 +28,23 @@ export async function copyText(text) {
|
|
|
24
28
|
ta.select();
|
|
25
29
|
ta.setSelectionRange(0, text.length);
|
|
26
30
|
const ok = document.execCommand('copy');
|
|
27
|
-
document.body.removeChild(ta);
|
|
28
31
|
if (ok)
|
|
29
32
|
return true;
|
|
30
33
|
}
|
|
31
34
|
catch {
|
|
32
35
|
/* 폴백으로 진행 */
|
|
33
36
|
}
|
|
37
|
+
finally {
|
|
38
|
+
// 성공·실패·던짐 어느 길이든 textarea 를 거두고 포커스를 되돌린다
|
|
39
|
+
if (ta?.parentNode)
|
|
40
|
+
ta.parentNode.removeChild(ta);
|
|
41
|
+
if (원래포커스 && 원래포커스 !== document.activeElement && typeof 원래포커스.focus === 'function' && 원래포커스.isConnected) {
|
|
42
|
+
try {
|
|
43
|
+
원래포커스.focus({ preventScroll: true });
|
|
44
|
+
}
|
|
45
|
+
catch { /* 무시 */ }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
34
48
|
}
|
|
35
49
|
// 2순위: Async Clipboard API
|
|
36
50
|
try {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* 곳곳에 흩어져 있어 여기 한 벌로 모은다.
|
|
12
12
|
*
|
|
13
13
|
* 문자열도 그대로 쓴다 — `throw '문구'` 로 던지는 자리가 있다. 끝내 못 읽는
|
|
14
|
-
* 값(숫자·null·빈 객체·빈 문구)일 때만 fallback 을 쓴다.
|
|
14
|
+
* 값(숫자·null·빈 객체·빈 문구)일 때만 fallback 을 쓴다. fallback 을 주지 않으면 지금
|
|
15
|
+
* 언어의 '알 수 없는 오류'(`common.unknownError`), 주면 그 문구 그대로.
|
|
15
16
|
*
|
|
16
17
|
* 회원 앱은 `src/libs/errorUtils.ts` 가 이것을 다시 내보낸다 — 한 벌만 둔다.
|
|
17
18
|
* @public
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
import i18n from 'i18next';
|
|
2
|
+
/**
|
|
3
|
+
* 못 읽을 때의 기본 문구 — 지금 언어로. 사전에 없거나 i18n 이 아직 안 섰으면 한국어.
|
|
4
|
+
*
|
|
5
|
+
* `src/i18n` 이 초기화하는 것도 i18next 의 **기본 인스턴스**다(묶음에서 i18next 는 external).
|
|
6
|
+
* 여기서 `../i18n` 을 가져오면 utils 묶음이 사전 JSON 과 init 부작용을 끌어안으므로 기본
|
|
7
|
+
* 인스턴스를 직접 본다. 초기화 전의 `i18n.t` 는 undefined 를 돌려준다.
|
|
8
|
+
*/
|
|
9
|
+
const 알수없는오류 = () => {
|
|
10
|
+
const 기본 = '알 수 없는 오류';
|
|
11
|
+
const s = typeof i18n?.t === 'function' ? i18n.t('common.unknownError', { defaultValue: 기본 }) : undefined;
|
|
12
|
+
return typeof s === 'string' && s ? s : 기본;
|
|
13
|
+
};
|
|
1
14
|
/**
|
|
2
15
|
* 잡은 값에서 **사람이 읽을 수 있는 원인**을 꺼낸다.
|
|
3
16
|
*
|
|
@@ -11,12 +24,16 @@
|
|
|
11
24
|
* 곳곳에 흩어져 있어 여기 한 벌로 모은다.
|
|
12
25
|
*
|
|
13
26
|
* 문자열도 그대로 쓴다 — `throw '문구'` 로 던지는 자리가 있다. 끝내 못 읽는
|
|
14
|
-
* 값(숫자·null·빈 객체·빈 문구)일 때만 fallback 을 쓴다.
|
|
27
|
+
* 값(숫자·null·빈 객체·빈 문구)일 때만 fallback 을 쓴다. fallback 을 주지 않으면 지금
|
|
28
|
+
* 언어의 '알 수 없는 오류'(`common.unknownError`), 주면 그 문구 그대로.
|
|
15
29
|
*
|
|
16
30
|
* 회원 앱은 `src/libs/errorUtils.ts` 가 이것을 다시 내보낸다 — 한 벌만 둔다.
|
|
17
31
|
* @public
|
|
18
32
|
*/
|
|
19
|
-
export function getErrorMessage(e, fallback
|
|
33
|
+
export function getErrorMessage(e, fallback) {
|
|
34
|
+
// 기본 문구는 부를 때마다 지금 언어로 만든다(모듈 로드 때 한 번 정하면 언어 전환을 못 따른다)
|
|
35
|
+
if (fallback === undefined)
|
|
36
|
+
fallback = 알수없는오류();
|
|
20
37
|
if (e instanceof Error)
|
|
21
38
|
return e.message || fallback;
|
|
22
39
|
if (typeof e === 'string')
|
|
@@ -26,7 +26,14 @@ export type ComputeDailySelectionOptions = {
|
|
|
26
26
|
counts?: TrainingCounts;
|
|
27
27
|
forceRefresh?: boolean;
|
|
28
28
|
};
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* 오늘·어제 세트를 뽑아 `TODAY_SETS`·`YESTERDAY_SETS` 에 채운다(`loadTrainingData` 가 부른다).
|
|
31
|
+
*
|
|
32
|
+
* **어제 세트는 "어제 뽑힌 세트" 가 아니다** — 캐시가 메모리에만 있어, 날이 바뀌면 어제 몫도 따로 새로 뽑는다.
|
|
33
|
+
* 사용자에게 보이는 오늘·어제 세트는 사용자와 날짜로 시드를 정하는 `useDailyTrainingSelection` 을 쓴다
|
|
34
|
+
* (같은 날 같은 사용자면 늘 같은 세트, 어제 세트도 어제와 같다).
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
30
37
|
export declare function computeDailySelection({ counts, forceRefresh }?: ComputeDailySelectionOptions): {
|
|
31
38
|
today: {
|
|
32
39
|
[k: string]: unknown[];
|
|
@@ -18,7 +18,14 @@ const DEFAULT_TRAINING_COUNTS = { LISTEN_IN_NOISE: 3, SHORT_STORY: 2, LONG_STORY
|
|
|
18
18
|
let _cachedCountsKey = null;
|
|
19
19
|
let _cachedTodayDate = null;
|
|
20
20
|
let _cachedSelection = null;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* 오늘·어제 세트를 뽑아 `TODAY_SETS`·`YESTERDAY_SETS` 에 채운다(`loadTrainingData` 가 부른다).
|
|
23
|
+
*
|
|
24
|
+
* **어제 세트는 "어제 뽑힌 세트" 가 아니다** — 캐시가 메모리에만 있어, 날이 바뀌면 어제 몫도 따로 새로 뽑는다.
|
|
25
|
+
* 사용자에게 보이는 오늘·어제 세트는 사용자와 날짜로 시드를 정하는 `useDailyTrainingSelection` 을 쓴다
|
|
26
|
+
* (같은 날 같은 사용자면 늘 같은 세트, 어제 세트도 어제와 같다).
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
22
29
|
export function computeDailySelection({ counts, forceRefresh } = {}) {
|
|
23
30
|
const c = Object.assign({}, DEFAULT_TRAINING_COUNTS, counts || {});
|
|
24
31
|
// 오늘의 훈련은 KST 자정에 바뀌어야 한다 — UTC 날짜를 쓰면 오전 9시에 바뀐다
|
|
@@ -83,19 +83,21 @@ export declare function wordHasSyllableMatching(word: string, parts: {
|
|
|
83
83
|
*/
|
|
84
84
|
export declare function isSyllablePattern(query: string): boolean;
|
|
85
85
|
/**
|
|
86
|
-
* @public 음절 패턴 미니 DSL 매칭. 쉼표(,)로 나눈 조각을
|
|
87
|
-
*
|
|
86
|
+
* @public 음절 패턴 미니 DSL 매칭. 쉼표(,)로 나눈 조각을 연속된 음절 자리들로 풀고, 그 자리들이 단어의
|
|
87
|
+
* **어느 위치에서든 연속으로** 맞으면 매칭된다(포함 매칭 — 단어 음절 수가 자리 수보다 많아도 된다).
|
|
88
|
+
* 한 자리는 다음 중 하나:
|
|
88
89
|
* - 빈 자리(쉼표 사이가 비어 있음) — 무관(아무 음절이나 허용).
|
|
89
|
-
* - 완성 음절 한 글자(예: '가') —
|
|
90
|
+
* - 완성 음절 한 글자(예: '가', '강') — 받침 없는 글자는 초성·중성만 같으면 되고(받침 무관: '수' → 수·술·숙),
|
|
91
|
+
* 받침 있는 글자는 정확히 같아야 함.
|
|
90
92
|
* - 자음/모음 한 글자(예: 'ㄴ', 'ㅏ') — 자음이면 그 자리 초성이, 모음이면 그 자리 중성이 정확히 그
|
|
91
93
|
* 자모여야 함(나머지는 무관).
|
|
92
|
-
* -
|
|
93
|
-
* 무관, 자모면 그
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* 예: '가,,다' →
|
|
97
|
-
* 예: '__ㄱ,고' →
|
|
98
|
-
* 예: '가ㄴ,,다'(= '가,ㄴ,,다'와 동일) → 4
|
|
94
|
+
* - 한 음절 템플릿(조각에 '_'가 하나라도 있을 때, 1~4글자) — 초성·중성·종성 순으로 읽고, '_'나 생략한 뒤
|
|
95
|
+
* 자리는 무관, 자모면 그 위치가 정확히 그 자모여야 함. 종성은 겹받침 두 자모('ㄹㄱ' = ㄺ)까지.
|
|
96
|
+
* 예: '__ㄱ' = 종성 ㄱ, 'ㄱ_' = 초성 ㄱ, 'ㄱ_ㄱ' = 초성 ㄱ·종성 ㄱ.
|
|
97
|
+
* '_' 없이 완성 음절·자음·모음을 이어 붙이면(예: '가ㄴ') 한 글자씩 각각 한 음절 자리로 분해된다.
|
|
98
|
+
* 예: '가,,다' → 첫 음절 '가'(받침 무관) · 가운데 무관 · 끝 음절 '다'인 연속 3음절을 포함한 단어.
|
|
99
|
+
* 예: '__ㄱ,고' → 종성 'ㄱ'인 음절 바로 뒤에 '고'가 오는 단어(먹고·박고구마 …).
|
|
100
|
+
* 예: '가ㄴ,,다'(= '가,ㄴ,,다'와 동일) → 연속 4음절 '가' · 초성 'ㄴ' · 무관 · '다'.
|
|
99
101
|
* 해석할 수 없는 글자(자음·모음·완성 음절이 아님)가 섞이면 그 자리는 항상 매칭 실패 처리되어 결과가
|
|
100
102
|
* 0건이 된다(조용히 무관 처리하지 않는다 — 제약이 무시된 채 결과가 나오는 오해를 막기 위해).
|
|
101
103
|
*/
|
|
@@ -117,8 +119,9 @@ export declare function patternToComposition(pattern: string): {
|
|
|
117
119
|
} | null;
|
|
118
120
|
/**
|
|
119
121
|
* @public 단어 검색 '글자 입력'의 형식 유효성 검사.
|
|
120
|
-
* 허용: 완성 음절('가'), 초성/중성 자모('ㄱ'·'ㅏ'),
|
|
121
|
-
* 쉼표(,) 구분(빈 자리는 '무관'). 그 외(예:
|
|
122
|
+
* 허용: 완성 음절('가'), 초성/중성 자모('ㄱ'·'ㅏ'), '_'가 든 한 음절 템플릿(1~4글자, 초성·중성·종성 순 —
|
|
123
|
+
* '__ㄱ'·'ㄱ_'·'ㄱ_ㄹㄱ'), 쉼표(,) 구분(빈 자리는 '무관'). 그 외(예: 5글자 이상 템플릿, 템플릿 자리에 맞지 않는
|
|
124
|
+
* 자모('ㅏ_'), 라틴 문자)는 무효.
|
|
122
125
|
* @returns 유효하면 `{ ok: true }`, 아니면 `{ ok: false }`(호출부가 안내 문구를 표시).
|
|
123
126
|
*/
|
|
124
127
|
export declare function validateSyllableSearchInput(input: string): {
|
|
@@ -398,19 +398,21 @@ function matchesPatternAtom(ch, atom) {
|
|
|
398
398
|
}
|
|
399
399
|
}
|
|
400
400
|
/**
|
|
401
|
-
* @public 음절 패턴 미니 DSL 매칭. 쉼표(,)로 나눈 조각을
|
|
402
|
-
*
|
|
401
|
+
* @public 음절 패턴 미니 DSL 매칭. 쉼표(,)로 나눈 조각을 연속된 음절 자리들로 풀고, 그 자리들이 단어의
|
|
402
|
+
* **어느 위치에서든 연속으로** 맞으면 매칭된다(포함 매칭 — 단어 음절 수가 자리 수보다 많아도 된다).
|
|
403
|
+
* 한 자리는 다음 중 하나:
|
|
403
404
|
* - 빈 자리(쉼표 사이가 비어 있음) — 무관(아무 음절이나 허용).
|
|
404
|
-
* - 완성 음절 한 글자(예: '가') —
|
|
405
|
+
* - 완성 음절 한 글자(예: '가', '강') — 받침 없는 글자는 초성·중성만 같으면 되고(받침 무관: '수' → 수·술·숙),
|
|
406
|
+
* 받침 있는 글자는 정확히 같아야 함.
|
|
405
407
|
* - 자음/모음 한 글자(예: 'ㄴ', 'ㅏ') — 자음이면 그 자리 초성이, 모음이면 그 자리 중성이 정확히 그
|
|
406
408
|
* 자모여야 함(나머지는 무관).
|
|
407
|
-
* -
|
|
408
|
-
* 무관, 자모면 그
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
* 예: '가,,다' →
|
|
412
|
-
* 예: '__ㄱ,고' →
|
|
413
|
-
* 예: '가ㄴ,,다'(= '가,ㄴ,,다'와 동일) → 4
|
|
409
|
+
* - 한 음절 템플릿(조각에 '_'가 하나라도 있을 때, 1~4글자) — 초성·중성·종성 순으로 읽고, '_'나 생략한 뒤
|
|
410
|
+
* 자리는 무관, 자모면 그 위치가 정확히 그 자모여야 함. 종성은 겹받침 두 자모('ㄹㄱ' = ㄺ)까지.
|
|
411
|
+
* 예: '__ㄱ' = 종성 ㄱ, 'ㄱ_' = 초성 ㄱ, 'ㄱ_ㄱ' = 초성 ㄱ·종성 ㄱ.
|
|
412
|
+
* '_' 없이 완성 음절·자음·모음을 이어 붙이면(예: '가ㄴ') 한 글자씩 각각 한 음절 자리로 분해된다.
|
|
413
|
+
* 예: '가,,다' → 첫 음절 '가'(받침 무관) · 가운데 무관 · 끝 음절 '다'인 연속 3음절을 포함한 단어.
|
|
414
|
+
* 예: '__ㄱ,고' → 종성 'ㄱ'인 음절 바로 뒤에 '고'가 오는 단어(먹고·박고구마 …).
|
|
415
|
+
* 예: '가ㄴ,,다'(= '가,ㄴ,,다'와 동일) → 연속 4음절 '가' · 초성 'ㄴ' · 무관 · '다'.
|
|
414
416
|
* 해석할 수 없는 글자(자음·모음·완성 음절이 아님)가 섞이면 그 자리는 항상 매칭 실패 처리되어 결과가
|
|
415
417
|
* 0건이 된다(조용히 무관 처리하지 않는다 — 제약이 무시된 채 결과가 나오는 오해를 막기 위해).
|
|
416
418
|
*/
|
|
@@ -423,7 +425,7 @@ export function matchesSyllablePattern(word, pattern) {
|
|
|
423
425
|
return false;
|
|
424
426
|
// '포함' 매칭 — 패턴(연속 음절 자리들)이 단어의 어느 위치에서든 연속으로 맞으면 통과.
|
|
425
427
|
// (단어 음절수 == 패턴 자리수를 요구하지 않는다. 예: '__ㄱ,고' → 첫음절 종성ㄱ+둘째음절 '고'를
|
|
426
|
-
// 포함한 모든 단어 =
|
|
428
|
+
// 포함한 모든 단어 = 먹고·박고구마… 길이 무관.)
|
|
427
429
|
for (let i = 0; i + atoms.length <= chars.length; i++) {
|
|
428
430
|
if (atoms.every((atom, j) => matchesPatternAtom(chars[i + j], atom)))
|
|
429
431
|
return true;
|
|
@@ -488,8 +490,9 @@ function isSearchAtomChar(ch) {
|
|
|
488
490
|
}
|
|
489
491
|
/**
|
|
490
492
|
* @public 단어 검색 '글자 입력'의 형식 유효성 검사.
|
|
491
|
-
* 허용: 완성 음절('가'), 초성/중성 자모('ㄱ'·'ㅏ'),
|
|
492
|
-
* 쉼표(,) 구분(빈 자리는 '무관'). 그 외(예:
|
|
493
|
+
* 허용: 완성 음절('가'), 초성/중성 자모('ㄱ'·'ㅏ'), '_'가 든 한 음절 템플릿(1~4글자, 초성·중성·종성 순 —
|
|
494
|
+
* '__ㄱ'·'ㄱ_'·'ㄱ_ㄹㄱ'), 쉼표(,) 구분(빈 자리는 '무관'). 그 외(예: 5글자 이상 템플릿, 템플릿 자리에 맞지 않는
|
|
495
|
+
* 자모('ㅏ_'), 라틴 문자)는 무효.
|
|
493
496
|
* @returns 유효하면 `{ ok: true }`, 아니면 `{ ok: false }`(호출부가 안내 문구를 표시).
|
|
494
497
|
*/
|
|
495
498
|
export function validateSyllableSearchInput(input) {
|
|
@@ -3,9 +3,10 @@ import type { ComponentType } from 'react';
|
|
|
3
3
|
* 정적으로 번들되는 아이콘 목록.
|
|
4
4
|
*
|
|
5
5
|
* 이 목록은 speechear·kscan 소스의 icon 리터럴(`icon="..."`, `icon: '...'`)을
|
|
6
|
-
* 훑어 만들었습니다. **여기 없는 이름은
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* 훑어 만들었습니다. **여기 없는 이름은 그려지지 않습니다** — 지연 로드도
|
|
7
|
+
* 하지 않고, 개발 모드에서 이름당 한 번 경고만 남깁니다(`lucideLoader.ts`).
|
|
8
|
+
* 새 아이콘을 쓰기 시작했는데 비어 보인다면 이 목록에 추가하세요.
|
|
9
|
+
* 소스의 icon 리터럴이 목록에 없으면 `test/lucideIconSet.test.tsx` 가 실패합니다.
|
|
9
10
|
*
|
|
10
11
|
* 왜 이렇게 하는가: 예전에는 `import * as lucide from 'lucide-react'` 로
|
|
11
12
|
* 통째로 가져와 이름으로 조회했습니다. 그러면 어떤 이름이든 들어올 수 있으니
|
|
@@ -185,9 +185,10 @@ import ThumbsUpIcon from 'lucide-react/dist/esm/icons/thumbs-up.mjs';
|
|
|
185
185
|
* 정적으로 번들되는 아이콘 목록.
|
|
186
186
|
*
|
|
187
187
|
* 이 목록은 speechear·kscan 소스의 icon 리터럴(`icon="..."`, `icon: '...'`)을
|
|
188
|
-
* 훑어 만들었습니다. **여기 없는 이름은
|
|
189
|
-
*
|
|
190
|
-
*
|
|
188
|
+
* 훑어 만들었습니다. **여기 없는 이름은 그려지지 않습니다** — 지연 로드도
|
|
189
|
+
* 하지 않고, 개발 모드에서 이름당 한 번 경고만 남깁니다(`lucideLoader.ts`).
|
|
190
|
+
* 새 아이콘을 쓰기 시작했는데 비어 보인다면 이 목록에 추가하세요.
|
|
191
|
+
* 소스의 icon 리터럴이 목록에 없으면 `test/lucideIconSet.test.tsx` 가 실패합니다.
|
|
191
192
|
*
|
|
192
193
|
* 왜 이렇게 하는가: 예전에는 `import * as lucide from 'lucide-react'` 로
|
|
193
194
|
* 통째로 가져와 이름으로 조회했습니다. 그러면 어떤 이름이든 들어올 수 있으니
|
|
@@ -3,6 +3,11 @@ export interface CreateTTSResolverOptions {
|
|
|
3
3
|
storagePrefix?: string;
|
|
4
4
|
voice?: string;
|
|
5
5
|
speed?: number;
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated 리졸버가 쓰지 않는다 — 파일 이름에도, `/api/tts` 요청에도 들어가지 않는다
|
|
8
|
+
* (그 개발 서버도 pitch 를 받지 않는다). `generator` 에 넘기는 opts 에 그대로 실려 갈 뿐이라,
|
|
9
|
+
* 음높이가 필요하면 generator 가 직접 다룬다. 쓰는 곳이 없어 구현하지 않았다.
|
|
10
|
+
*/
|
|
6
11
|
pitch?: number;
|
|
7
12
|
/** Optional function to generate/upload TTS audio when missing. Should return a public URL when done. */
|
|
8
13
|
generator?: (text: string, destPath: string, opts?: CreateTTSResolverOptions) => Promise<string | null>;
|
|
@@ -16,7 +16,7 @@ export declare function phonemePartsOf(word: string): string[];
|
|
|
16
16
|
/**
|
|
17
17
|
* @public 합치기 문제 **표시** 문장 — 미리보기·러너 공용. 음절/음소 모두 원문(자모는 자모 그대로: 'ㄱ','ㅏ')로 표시한다.
|
|
18
18
|
* TTS로 읽을 땐 자모를 낱자 이름으로 바꿔야 하지만(그건 jamoToNamesForSpeech가 합성 경로에서 처리), 화면 표기는 자모 유지.
|
|
19
|
-
* 예: ['아','냐'] → `"아"와 "냐"를 합치면 어떤 단어가 되나요?` / ['ㄱ','ㅏ'] → `"ㄱ"
|
|
19
|
+
* 예: ['아','냐'] → `"아"와 "냐"를 합치면 어떤 단어가 되나요?` / ['ㄱ','ㅏ'] → `"ㄱ"과 "ㅏ"를 합치면 어떤 소리가 되나요?`
|
|
20
20
|
*/
|
|
21
21
|
export declare function mergeQuestionText(parts: string[], phoneme?: boolean): string;
|
|
22
22
|
/**
|
|
@@ -54,7 +54,7 @@ declare function pronOf(word: string, pronMap: Map<string, string>): string;
|
|
|
54
54
|
export declare function syllablePron(ch: string): string;
|
|
55
55
|
/**
|
|
56
56
|
* @public 음절의 실제 소리(표준발음). pronMap에 있으면 그것, 없으면(합성 음절) 끝소리 규칙 발음, 그래도 없으면 철자.
|
|
57
|
-
* (예: '
|
|
57
|
+
* (예: '밖'·'밗'은 모두 [박] — 소리 기준 중복 판정에 쓴다.)
|
|
58
58
|
*/
|
|
59
59
|
export declare function soundOf(syll: string, pronMap: Map<string, string>): string;
|
|
60
60
|
/** @public comp에서 실제로 선택된 파트 1개(초성 우선)를 반환. 없으면 null. */
|
|
@@ -68,7 +68,7 @@ export declare function syllMatchesComp(s: Syllable, comp: CompositionFilter): b
|
|
|
68
68
|
export declare function matchSyllables(word: string, pronMap: Map<string, string>, phonemeView: 'char' | 'pron'): (Syllable | null)[];
|
|
69
69
|
/**
|
|
70
70
|
* @public 정답 음절(분해 cs)의 지정 파트를 다른 음소로 바꿔 대립쌍 음절 n개 생성.
|
|
71
|
-
* 중복 배제는 '실제 발음(soundOf)' 기준 — 소리가 같은 오답(
|
|
71
|
+
* 중복 배제는 '실제 발음(soundOf)' 기준 — 소리가 같은 오답(밖/밗→박)이 함께 나오지 않게 한다.
|
|
72
72
|
*/
|
|
73
73
|
export declare function syllableContrasts(cs: Syllable, part: 'cho' | 'jung' | 'jong', correct: string, n: number, pronMap: Map<string, string>): string[];
|
|
74
74
|
/** @public 훈련 문제 한 개(편집기·러너 공용). distractors[0]=정답, 이후=오답. */
|
|
@@ -78,7 +78,7 @@ export function phonemePartsOf(word) {
|
|
|
78
78
|
/**
|
|
79
79
|
* @public 합치기 문제 **표시** 문장 — 미리보기·러너 공용. 음절/음소 모두 원문(자모는 자모 그대로: 'ㄱ','ㅏ')로 표시한다.
|
|
80
80
|
* TTS로 읽을 땐 자모를 낱자 이름으로 바꿔야 하지만(그건 jamoToNamesForSpeech가 합성 경로에서 처리), 화면 표기는 자모 유지.
|
|
81
|
-
* 예: ['아','냐'] → `"아"와 "냐"를 합치면 어떤 단어가 되나요?` / ['ㄱ','ㅏ'] → `"ㄱ"
|
|
81
|
+
* 예: ['아','냐'] → `"아"와 "냐"를 합치면 어떤 단어가 되나요?` / ['ㄱ','ㅏ'] → `"ㄱ"과 "ㅏ"를 합치면 어떤 소리가 되나요?`
|
|
82
82
|
*/
|
|
83
83
|
export function mergeQuestionText(parts, phoneme = false) {
|
|
84
84
|
const p = parts.filter(Boolean);
|
|
@@ -190,7 +190,7 @@ export function syllablePron(ch) {
|
|
|
190
190
|
}
|
|
191
191
|
/**
|
|
192
192
|
* @public 음절의 실제 소리(표준발음). pronMap에 있으면 그것, 없으면(합성 음절) 끝소리 규칙 발음, 그래도 없으면 철자.
|
|
193
|
-
* (예: '
|
|
193
|
+
* (예: '밖'·'밗'은 모두 [박] — 소리 기준 중복 판정에 쓴다.)
|
|
194
194
|
*/
|
|
195
195
|
export function soundOf(syll, pronMap) {
|
|
196
196
|
return pronMap.get(syll) || syllablePron(syll) || syll;
|
|
@@ -227,13 +227,17 @@ export function matchSyllables(word, pronMap, phonemeView) {
|
|
|
227
227
|
}
|
|
228
228
|
/**
|
|
229
229
|
* @public 정답 음절(분해 cs)의 지정 파트를 다른 음소로 바꿔 대립쌍 음절 n개 생성.
|
|
230
|
-
* 중복 배제는 '실제 발음(soundOf)' 기준 — 소리가 같은 오답(
|
|
230
|
+
* 중복 배제는 '실제 발음(soundOf)' 기준 — 소리가 같은 오답(밖/밗→박)이 함께 나오지 않게 한다.
|
|
231
231
|
*/
|
|
232
232
|
export function syllableContrasts(cs, part, correct, n, pronMap) {
|
|
233
233
|
const arr = part === 'cho' ? CHOSEONG : part === 'jung' ? JUNGSEONG : JONGSEONG;
|
|
234
234
|
const cur = cs[part];
|
|
235
235
|
const out = [];
|
|
236
|
-
|
|
236
|
+
// 중성 대립은 발음 자료의 모음 병합(ㅔ→ㅐ·ㅖ→ㅒ·ㅚ/ㅞ→ㅙ)을 소리 열쇠에도 적용한다.
|
|
237
|
+
// 자료에 없는 만든 음절(예: '에')은 soundOf 가 철자 모음을 그대로 돌려줘, 정답 '애' 옆에 같은 소리 '에'가 오답으로 나왔다.
|
|
238
|
+
// (초성·종성 대립은 모음이 모두 같아 병합이 결과를 바꾸지 않으므로 손대지 않는다.)
|
|
239
|
+
const soundKey = (syll) => (part === 'jung' ? mergeVowelSound(soundOf(syll, pronMap)) : soundOf(syll, pronMap));
|
|
240
|
+
const usedSound = new Map([[soundKey(correct), -1]]); // 소리 → out 자리(-1 = 정답)
|
|
237
241
|
const idxs = shuffled(arr.map((_, i) => i).filter(i => i !== cur
|
|
238
242
|
&& (part !== 'cho' || arr[i] !== 'ㅇ') // 초성 대립은 무음(ㅇ) 제외 — 구조가 바뀜
|
|
239
243
|
&& (part !== 'jong' || i !== 0))); // 종성 대립은 '받침 없음'(0) 제외 — 구조가 바뀜
|
|
@@ -243,14 +247,31 @@ export function syllableContrasts(cs, part, correct, n, pronMap) {
|
|
|
243
247
|
const syll = part === 'cho' ? composeSyllable(idx, cs.jung, cs.jong)
|
|
244
248
|
: part === 'jung' ? composeSyllable(cs.cho, idx, cs.jong)
|
|
245
249
|
: composeSyllable(cs.cho, cs.jung, idx);
|
|
246
|
-
const p =
|
|
247
|
-
|
|
250
|
+
const p = soundKey(syll);
|
|
251
|
+
const prev = usedSound.get(p);
|
|
252
|
+
if (prev !== undefined) {
|
|
253
|
+
// 병합으로 같은 소리가 된 둘 중 자료에 없는 만든 음절이 먼저 자리를 차지했으면 자료에 있는 음절로 바꿔 끼운다
|
|
254
|
+
// — 실단어만 남기는 소비처(탈락 대립 등)가 병합 탓에 실단어를 잃지 않게.
|
|
255
|
+
if (part === 'jung' && prev >= 0 && pronMap.has(syll) && !pronMap.has(out[prev]))
|
|
256
|
+
out[prev] = syll;
|
|
248
257
|
continue;
|
|
249
|
-
|
|
258
|
+
}
|
|
259
|
+
usedSound.set(p, out.length);
|
|
250
260
|
out.push(syll);
|
|
251
261
|
}
|
|
252
262
|
return out;
|
|
253
263
|
}
|
|
264
|
+
/** 발음 자료의 모음 병합 규약 — ㅔ→ㅐ, ㅖ→ㅒ, ㅚ·ㅞ→ㅙ. 1음절 한글이 아니면 그대로. */
|
|
265
|
+
const _VOWEL_MERGE = { 'ㅔ': 'ㅐ', 'ㅖ': 'ㅒ', 'ㅚ': 'ㅙ', 'ㅞ': 'ㅙ' };
|
|
266
|
+
function mergeVowelSound(syll) {
|
|
267
|
+
if ([...syll].length !== 1)
|
|
268
|
+
return syll;
|
|
269
|
+
const s = parseSyllable(syll);
|
|
270
|
+
if (!s)
|
|
271
|
+
return syll;
|
|
272
|
+
const to = _VOWEL_MERGE[JUNGSEONG[s.jung]];
|
|
273
|
+
return to ? composeSyllable(s.cho, JUNGSEONG.indexOf(to), s.jong) : syll;
|
|
274
|
+
}
|
|
254
275
|
/**
|
|
255
276
|
* @public 2음절 단어 → [문제단어, 남는 1음절(정답), 대립쌍 오답 2]. 정답·오답 모두 '실단어'(wordSet)만 인정.
|
|
256
277
|
* 음소가 든 음절(T)을 남기고 다른 음절 탈락 → 남는 1음절이 실단어이고, 그 음절의 초성/중성/종성 대립 실단어가
|