@ihabdevteam/core 0.323.0 → 0.325.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 +77 -0
- package/dist/components.cjs.js +182 -182
- package/dist/hooks.cjs.js +3 -3
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core.cjs.js +185 -185
- package/dist/ihabdevteam-core.d.ts +2 -2
- package/dist/packages/core/src/components/Calendar.js +2 -1
- package/dist/packages/core/src/components/DatePicker.js +3 -2
- package/dist/packages/core/src/components/useBarChartControl.js +3 -2
- package/dist/packages/core/src/i18n/locales/en.json +1 -1
- package/dist/packages/core/src/i18n/locales/ko.json +1 -1
- package/dist/packages/core/src/utils/date.d.ts +27 -0
- package/dist/packages/core/src/utils/date.js +37 -0
- package/dist/packages/core/src/utils/phonemePairSearch.js +13 -5
- package/dist/packages/core/src/utils/phonemePairSearchTypes.d.ts +2 -0
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +1 -1
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +23 -3
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +3 -3
- package/package.json +1 -1
|
@@ -1678,7 +1678,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1678
1678
|
*
|
|
1679
1679
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1680
1680
|
*/
|
|
1681
|
-
export declare const CORE_VERSION = "0.
|
|
1681
|
+
export declare const CORE_VERSION = "0.325.0";
|
|
1682
1682
|
|
|
1683
1683
|
/** @public */
|
|
1684
1684
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -10700,7 +10700,7 @@ export declare interface WordAttributeFilterKeys {
|
|
|
10700
10700
|
}
|
|
10701
10701
|
|
|
10702
10702
|
/** @public 공용 속성 필터 섹션 코드 */
|
|
10703
|
-
export declare type WordAttributeSectionCode = 'grade' | 'syllables' | 'pos' | 'wordType';
|
|
10703
|
+
export declare type WordAttributeSectionCode = 'grade' | 'syllables' | 'pos' | 'wordType' | 'field';
|
|
10704
10704
|
|
|
10705
10705
|
/** 정답 단어 구성 선택 상태 (정답 단어에만 적용 — 오답은 대립 관계로 결정) */
|
|
10706
10706
|
export declare interface WordComposition {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useMemo, useState, useRef } from 'react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { yearMonthLabel } from '../utils/date';
|
|
5
6
|
import Icon from './Icon';
|
|
6
7
|
import { formatTimeHM } from '../utils/format';
|
|
7
8
|
import { todayYmdOr, formatDateLocale } from '../utils/date';
|
|
@@ -55,7 +56,7 @@ export default function Calendar({ visits = [], onVisitClick, onVisitMove, onDay
|
|
|
55
56
|
setMonth(m => m + 1); }, [month]);
|
|
56
57
|
const fmtDate = (d) => `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
|
57
58
|
const isToday = (d) => fmtDate(d) === todayYmd;
|
|
58
|
-
return (_jsxs("div", { children: [_jsxs("div", { className: "calendar-nav", children: [_jsx("button", { type: "button", className: "calendar-nav__button", onClick: prev, "aria-label": t('calendar.prevMonth', '이전 달'), children: _jsx(Icon, { icon: "chevron-left", label: "", className: "", svgClassName: "calendar-nav__button-icon", size: 18 }) }), _jsx("span", { className: "calendar-nav__title h5", "aria-live": "polite", children: t
|
|
59
|
+
return (_jsxs("div", { children: [_jsxs("div", { className: "calendar-nav", children: [_jsx("button", { type: "button", className: "calendar-nav__button", onClick: prev, "aria-label": t('calendar.prevMonth', '이전 달'), children: _jsx(Icon, { icon: "chevron-left", label: "", className: "", svgClassName: "calendar-nav__button-icon", size: 18 }) }), _jsx("span", { className: "calendar-nav__title h5", "aria-live": "polite", children: yearMonthLabel(t, i18n.language, year, month + 1) }), _jsx("button", { type: "button", className: "calendar-nav__button", onClick: next, "aria-label": t('calendar.nextMonth', '다음 달'), children: _jsx(Icon, { icon: "chevron-right", label: "", className: "", svgClassName: "calendar-nav__button-icon", size: 18 }) }), _jsxs("div", { className: "calendar-nav__legend", children: [_jsx("span", { className: "calendar-nav__legend-dot calendar-nav__legend-dot--main" }), _jsx("span", { className: "caption-1", children: t('calendar.legend.visited', '방문') }), _jsx("span", { className: "calendar-nav__legend-dot calendar-nav__legend-dot--sub" }), _jsx("span", { className: "caption-1", children: t('calendar.legend.scheduled', '예약') }), _jsx("span", { className: "calendar-nav__legend-dot calendar-nav__legend-dot--disabled" }), _jsx("span", { className: "caption-1", children: t('calendar.legend.noVisit', '미방문') })] })] }), _jsxs("div", { className: "calendar-grid", children: [DOW.map(d => _jsx("div", { className: "calendar-day-header body-3", children: d }, d)), cells.map((d, i) => {
|
|
59
60
|
if (d === null)
|
|
60
61
|
return _jsx("div", { className: "calendar-cell empty" }, `empty-${i}`);
|
|
61
62
|
const day = d;
|
|
@@ -8,6 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
import { cx } from '../utils/cx';
|
|
9
9
|
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
10
10
|
import { useTranslation } from 'react-i18next';
|
|
11
|
+
import { yearMonthLabel } from '../utils/date';
|
|
11
12
|
import { createPortal } from 'react-dom';
|
|
12
13
|
import { todayYmdOr } from '../utils/date';
|
|
13
14
|
import Calendar from 'lucide-react/dist/esm/icons/calendar.mjs';
|
|
@@ -48,7 +49,7 @@ function buildYMOptions(viewY, ymLabel, min, max) {
|
|
|
48
49
|
return opts;
|
|
49
50
|
}
|
|
50
51
|
export default function DatePicker({ value, onChange, size = 48, min, max, disabled, placeholder: placeholderProp, className, today, }) {
|
|
51
|
-
const { t } = useTranslation();
|
|
52
|
+
const { t, i18n } = useTranslation();
|
|
52
53
|
/* 기본 안내 문구도 사전에서 — 예전에는 '날짜 선택' 이 박혀 있어 영어 화면에서도 한국어였다.
|
|
53
54
|
달력 판의 이름(`datePicker.chooseDate`)과 같은 말이라 같은 열쇠를 쓴다. */
|
|
54
55
|
const placeholder = placeholderProp ?? t('datePicker.chooseDate', '날짜 선택');
|
|
@@ -254,7 +255,7 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
254
255
|
for (let d = 1; d <= daysInMonth(viewY, viewM); d++)
|
|
255
256
|
cells.push(d);
|
|
256
257
|
const iconSize = size === 64 ? 20 : size === 40 ? 16 : 18;
|
|
257
|
-
const ymOptions = buildYMOptions(viewY, (year, month) => t
|
|
258
|
+
const ymOptions = buildYMOptions(viewY, (year, month) => yearMonthLabel(t, i18n.language, year, month), min, max);
|
|
258
259
|
const ymValue = `${viewY}-${String(viewM + 1).padStart(2, '0')}`;
|
|
259
260
|
const dropdown = open ? createPortal(_jsxs("div", { ref: menuRef, id: panelId, className: "date-picker__dropdown", style: dropdownStyle, "data-tooltip-ignore": true, role: "dialog", "aria-label": t('datePicker.chooseDate', '날짜 선택'), tabIndex: -1, children: [_jsxs("div", { className: "date-picker__nav", children: [_jsx("button", { type: "button", className: "date-picker__nav-btn", onClick: prevMonth, "aria-label": t('calendar.prevMonth', '이전 달'), children: _jsx(ChevronLeft, { size: 16, strokeWidth: 2 }) }), _jsx(Dropdown, { className: "date-picker__nav-dropdown", ariaLabel: t('datePicker.selectYearMonth', '연월 선택'), options: ymOptions, value: ymValue, onChange: (v) => {
|
|
260
261
|
const [y, m] = v.split('-');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { yearMonthLabel } from '../utils/date';
|
|
4
5
|
import { firstMondayOfMonth, todayWeekValue, monthOptionsNow, weekLabel } from './barChartData';
|
|
5
6
|
/**
|
|
6
7
|
* BarChart 기간 상태/옵션/네비게이션을 캡슐화한 훅.
|
|
@@ -8,7 +9,7 @@ import { firstMondayOfMonth, todayWeekValue, monthOptionsNow, weekLabel } from '
|
|
|
8
9
|
* @public
|
|
9
10
|
*/
|
|
10
11
|
export function useBarChartControl(data) {
|
|
11
|
-
const { t } = useTranslation();
|
|
12
|
+
const { t, i18n } = useTranslation();
|
|
12
13
|
const [period, setPeriod] = useState('week');
|
|
13
14
|
// 달 옵션·오늘은 자료가 바뀔 때 다시 센다 — 모듈 로드 때 한 번 세면 자정을 넘긴 탭에서 이번 달·오늘 주가 옛날에 굳었다
|
|
14
15
|
const DATA_MONTH_OPTIONS = useMemo(() => monthOptionsNow().filter(o => o.value in data), [data]);
|
|
@@ -25,7 +26,7 @@ export function useBarChartControl(data) {
|
|
|
25
26
|
return opts;
|
|
26
27
|
}, [DATA_MONTH_OPTIONS]);
|
|
27
28
|
const weekOptionLabel = useCallback((o) => {
|
|
28
|
-
const m = t
|
|
29
|
+
const m = yearMonthLabel(t, i18n.language, o.year, o.month);
|
|
29
30
|
return `${m} ${weekLabel(t, o.week)}`;
|
|
30
31
|
}, [t]);
|
|
31
32
|
const monthOptions = useMemo(() => DATA_MONTH_OPTIONS.map(o => ({
|
|
@@ -115,7 +115,6 @@
|
|
|
115
115
|
"Sat"
|
|
116
116
|
],
|
|
117
117
|
"weekLabel": "Week {{ord}}",
|
|
118
|
-
"yearMonth": "{{month}}/{{year}}",
|
|
119
118
|
"prevMonth": "Previous month",
|
|
120
119
|
"addOnDate": "Add a visit on {{date}}",
|
|
121
120
|
"nextMonth": "Next month",
|
|
@@ -922,6 +921,7 @@
|
|
|
922
921
|
"modifier": "Adverb/Determiner"
|
|
923
922
|
},
|
|
924
923
|
"wordType": "Word Type",
|
|
924
|
+
"field": "Semantic Category",
|
|
925
925
|
"type": {
|
|
926
926
|
"native": "Native Korean",
|
|
927
927
|
"sino": "Sino-Korean",
|
|
@@ -120,7 +120,6 @@
|
|
|
120
120
|
"일곱째"
|
|
121
121
|
],
|
|
122
122
|
"weekLabel": "{{ord}}주",
|
|
123
|
-
"yearMonth": "{{year}}년 {{month}}월",
|
|
124
123
|
"prevMonth": "이전 달",
|
|
125
124
|
"addOnDate": "{{date}}에 방문 등록",
|
|
126
125
|
"nextMonth": "다음 달",
|
|
@@ -909,6 +908,7 @@
|
|
|
909
908
|
"modifier": "수식언(부사, 관형사)"
|
|
910
909
|
},
|
|
911
910
|
"wordType": "유형",
|
|
911
|
+
"field": "의미범주",
|
|
912
912
|
"type": {
|
|
913
913
|
"native": "고유어",
|
|
914
914
|
"sino": "한자어",
|
|
@@ -12,6 +12,33 @@ export declare function formatDateLongKo(iso: string): string;
|
|
|
12
12
|
* @param lang useTranslation()의 i18n.language
|
|
13
13
|
*/
|
|
14
14
|
export declare function formatDateLocale(iso: string, lang: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* **연·월 이름표** — `2026년 10월` · `October 2026`. @public
|
|
17
|
+
*
|
|
18
|
+
* 사장님 지시(2026-10-14). 전에는 사전 글귀(`calendar.yearMonth`)가 영어에서
|
|
19
|
+
* `{{month}}/{{year}}` 이라 **`10/2026`** 으로 떴다(말귀_1 이 훈련 현황 도구줄에서 봤다).
|
|
20
|
+
* 달 이름은 틀로는 못 만든다 — 열두 이름을 두 말로 적거나, 브라우저가 든 것을 쓰거나다.
|
|
21
|
+
* **브라우저 쪽을 쓴다**: 한 줄이고 두 말이 같이 맞고, 말이 늘어도 따라온다.
|
|
22
|
+
*
|
|
23
|
+
* `month` 는 **1 부터**다(화면이 쓰는 꼴). `Date` 의 0 부터와 헷갈리기 쉬워 여기서 받아
|
|
24
|
+
* 한 번만 내린다 — 부르는 쪽 셋이 저마다 `+1`·`-1` 을 하면 그중 하나가 어긋난다.
|
|
25
|
+
*
|
|
26
|
+
* **사전에 그 열쇠가 있으면 그쪽이 이긴다.** 그 글귀는 부르는 쪽이 덮으라고 열어 둔
|
|
27
|
+
* 자리다(`DatePicker` 머리글). 코어 사전에서는 뺐으므로, 안 덮으면 여기로 온다.
|
|
28
|
+
*/
|
|
29
|
+
export declare function formatYearMonth(year: number, month: number, lang: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* **연·월 이름표 — 사전이 있으면 사전, 없으면 브라우저.** @public
|
|
32
|
+
*
|
|
33
|
+
* `calendar.yearMonth` 는 **부르는 쪽이 덮으라고 열어 둔 자리**다(`DatePicker` 머리글).
|
|
34
|
+
* 코어 사전에서는 뺐으므로 안 덮으면 `formatYearMonth` 로 온다.
|
|
35
|
+
*
|
|
36
|
+
* **이 가름을 한 자리에 둔다.** 쓰는 곳이 셋(`Calendar` 머리줄 · `DatePicker` 연월 고르개 ·
|
|
37
|
+
* 막대그래프 이름표)인데 저마다 적으면 그중 하나가 안 따라온다 — 이 저장소에서 여러 번 겪은
|
|
38
|
+
* 꼴이다. `defaultValue: ''` 로 묻는 까닭은 `exists()` 없이도 "사전에 있나" 를 가를 수
|
|
39
|
+
* 있어서다(없으면 빈 글자가 온다).
|
|
40
|
+
*/
|
|
41
|
+
export declare function yearMonthLabel(t: (key: string, opts?: Record<string, unknown>) => string, lang: string, year: number, month: number): string;
|
|
15
42
|
/** formatDateLongKo의 로케일 인식 버전 — en: "January 15, 2024, 03:30 PM" */
|
|
16
43
|
export declare function formatDateLongLocale(iso: string, lang: string): string;
|
|
17
44
|
/** "2024. 01. 15. 오후 03:30" (2-digit month + time) */
|
|
@@ -75,6 +75,43 @@ export function formatDateLocale(iso, lang) {
|
|
|
75
75
|
return '';
|
|
76
76
|
return d.toLocaleDateString(locale, { timeZone: KST, year: 'numeric', month: 'short', day: 'numeric' });
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* **연·월 이름표** — `2026년 10월` · `October 2026`. @public
|
|
80
|
+
*
|
|
81
|
+
* 사장님 지시(2026-10-14). 전에는 사전 글귀(`calendar.yearMonth`)가 영어에서
|
|
82
|
+
* `{{month}}/{{year}}` 이라 **`10/2026`** 으로 떴다(말귀_1 이 훈련 현황 도구줄에서 봤다).
|
|
83
|
+
* 달 이름은 틀로는 못 만든다 — 열두 이름을 두 말로 적거나, 브라우저가 든 것을 쓰거나다.
|
|
84
|
+
* **브라우저 쪽을 쓴다**: 한 줄이고 두 말이 같이 맞고, 말이 늘어도 따라온다.
|
|
85
|
+
*
|
|
86
|
+
* `month` 는 **1 부터**다(화면이 쓰는 꼴). `Date` 의 0 부터와 헷갈리기 쉬워 여기서 받아
|
|
87
|
+
* 한 번만 내린다 — 부르는 쪽 셋이 저마다 `+1`·`-1` 을 하면 그중 하나가 어긋난다.
|
|
88
|
+
*
|
|
89
|
+
* **사전에 그 열쇠가 있으면 그쪽이 이긴다.** 그 글귀는 부르는 쪽이 덮으라고 열어 둔
|
|
90
|
+
* 자리다(`DatePicker` 머리글). 코어 사전에서는 뺐으므로, 안 덮으면 여기로 온다.
|
|
91
|
+
*/
|
|
92
|
+
export function formatYearMonth(year, month, lang) {
|
|
93
|
+
/* 달이 범위 밖이면 `Date` 가 해를 넘겨 버린다(13월 → 다음 해 1월). 조용히 딴 해를
|
|
94
|
+
보여 주느니 받은 그대로 적는다. */
|
|
95
|
+
if (!Number.isFinite(year) || !Number.isFinite(month) || month < 1 || month > 12)
|
|
96
|
+
return `${year}-${month}`;
|
|
97
|
+
const locale = lang && lang.startsWith('ko') ? 'ko-KR' : lang || 'en-US';
|
|
98
|
+
return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'long' }).format(new Date(year, month - 1, 1));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* **연·월 이름표 — 사전이 있으면 사전, 없으면 브라우저.** @public
|
|
102
|
+
*
|
|
103
|
+
* `calendar.yearMonth` 는 **부르는 쪽이 덮으라고 열어 둔 자리**다(`DatePicker` 머리글).
|
|
104
|
+
* 코어 사전에서는 뺐으므로 안 덮으면 `formatYearMonth` 로 온다.
|
|
105
|
+
*
|
|
106
|
+
* **이 가름을 한 자리에 둔다.** 쓰는 곳이 셋(`Calendar` 머리줄 · `DatePicker` 연월 고르개 ·
|
|
107
|
+
* 막대그래프 이름표)인데 저마다 적으면 그중 하나가 안 따라온다 — 이 저장소에서 여러 번 겪은
|
|
108
|
+
* 꼴이다. `defaultValue: ''` 로 묻는 까닭은 `exists()` 없이도 "사전에 있나" 를 가를 수
|
|
109
|
+
* 있어서다(없으면 빈 글자가 온다).
|
|
110
|
+
*/
|
|
111
|
+
export function yearMonthLabel(t, lang, year, month) {
|
|
112
|
+
const fromDict = t('calendar.yearMonth', { year, month, defaultValue: '' });
|
|
113
|
+
return fromDict || formatYearMonth(year, month, lang);
|
|
114
|
+
}
|
|
78
115
|
/** formatDateLongKo의 로케일 인식 버전 — en: "January 15, 2024, 03:30 PM" */
|
|
79
116
|
export function formatDateLongLocale(iso, lang) {
|
|
80
117
|
const locale = lang.startsWith('ko') ? 'ko-KR' : 'en-US';
|
|
@@ -127,6 +127,7 @@ const DEFAULT_PAIR_FILTER_KEYS = {
|
|
|
127
127
|
syllable5plus: '5음절 이상',
|
|
128
128
|
partOfSpeech: '품사',
|
|
129
129
|
wordType: '유형',
|
|
130
|
+
field: '의미범주',
|
|
130
131
|
posMap: POS_GROUP_MAP_KO,
|
|
131
132
|
typeMap: WORD_TYPE_MAP_KO,
|
|
132
133
|
};
|
|
@@ -135,13 +136,14 @@ function collect(filterSelected, keys) {
|
|
|
135
136
|
const hit = collected.get(filterSelected);
|
|
136
137
|
if (hit && hit.keys === keys)
|
|
137
138
|
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 };
|
|
139
|
+
const value = { grades: [], syls: [], poses: [], types: [], fields: [] };
|
|
140
|
+
const bySection = { grade: value.grades, syllables: value.syls, pos: value.poses, wordType: value.types, field: value.fields };
|
|
140
141
|
for (const [label, raw] of Object.entries(filterSelected ?? {})) {
|
|
141
142
|
/* 섹션은 넘긴 keys 의 라벨이면 그것, 아니면 어느 언어·옛 라벨·코드든 읽는다(wordAttributeSectionOf) */
|
|
142
143
|
const s = label === keys.difficulty ? 'grade' : label === keys.syllables ? 'syllables'
|
|
143
144
|
: label === keys.partOfSpeech ? 'pos' : label === keys.wordType ? 'wordType'
|
|
144
|
-
:
|
|
145
|
+
: label === keys.field ? 'field'
|
|
146
|
+
: wordAttributeSectionOf(label);
|
|
145
147
|
if (!s)
|
|
146
148
|
continue;
|
|
147
149
|
for (const v of Array.isArray(raw) ? raw : typeof raw === 'string' ? [raw] : [])
|
|
@@ -193,8 +195,14 @@ export function applyFilters(filterSelected, words, keys = DEFAULT_PAIR_FILTER_K
|
|
|
193
195
|
return words.filter(entry => {
|
|
194
196
|
if (!matchesFilters({ grade: entry.grade, syllables: [...entry.word].length, pos: entry.pos, wordType: entry.wordType }, filterSelected, keys))
|
|
195
197
|
return false;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
/* **의미범주도 다른 넷과 같은 길로 읽는다**(사장님 지시 2026-10-14, 말귀 실측으로 다리는
|
|
199
|
+
필요 없음이 확인됐다 — 앱 소스 0건·저장 구성 416줄 0건).
|
|
200
|
+
|
|
201
|
+
전에는 `filterSelected['의미범주']` 로 **그 다섯 글자만** 찾았다. 다른 절은 어느 언어
|
|
202
|
+
라벨이든 읽는데 이 하나만 안 그랬고, 영어 라벨로 부르면 **이 필터만 조용히 안 걸렸다**
|
|
203
|
+
(에러도 없다). 같은 꼴을 등급·음절에서 이미 겪어 고쳤던 자리다. */
|
|
204
|
+
const { fields } = collect(filterSelected, keys);
|
|
205
|
+
if (fields.length) {
|
|
198
206
|
const normalized = (entry.field ?? '').replace(/전문\(|\)/g, '');
|
|
199
207
|
if (!fields.some(f => normalized.includes(f)))
|
|
200
208
|
return false;
|
|
@@ -25,6 +25,8 @@ export interface PairFilterKeys {
|
|
|
25
25
|
syllable5plus: string;
|
|
26
26
|
partOfSpeech: string;
|
|
27
27
|
wordType: string;
|
|
28
|
+
/** 의미범주 절의 이름표. **안 줘도 된다** — 안 주면 어느 언어 라벨이든 사전으로 읽는다(0.324.0). */
|
|
29
|
+
field?: string;
|
|
28
30
|
posMap: Record<string, string[]>;
|
|
29
31
|
typeMap: Record<string, string>;
|
|
30
32
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @public 공용 속성 필터 섹션 코드 */
|
|
2
|
-
export type WordAttributeSectionCode = 'grade' | 'syllables' | 'pos' | 'wordType';
|
|
2
|
+
export type WordAttributeSectionCode = 'grade' | 'syllables' | 'pos' | 'wordType' | 'field';
|
|
3
3
|
/**
|
|
4
4
|
* **등급이 빈 단어를 가리키는 코드** — `'none'`((205), 사장님 지시 2026-10-12).
|
|
5
5
|
*
|
|
@@ -11,6 +11,18 @@
|
|
|
11
11
|
* (useWordTrainingFilter 의 POS_CODES·toAttrCodes 와 같은 코드다.)
|
|
12
12
|
*/
|
|
13
13
|
import i18n from '../i18n';
|
|
14
|
+
/**
|
|
15
|
+
* **값이 코드가 아닌 절** — 지금은 `field`(의미범주) 하나다.
|
|
16
|
+
*
|
|
17
|
+
* 다른 넷은 값도 코드다(`'1'`·`'5+'`·`'noun'`·`'sino'`). 이 절의 값은 **사전이 들고 있는
|
|
18
|
+
* 분야 글자 자신**(`의학`·`법률`…)이라 미리 셀 수가 없고, 맞출 때도 같은지가 아니라
|
|
19
|
+
* **들어 있는지**로 본다(`entry.field` 가 `전문(의학)` 꼴이다).
|
|
20
|
+
*
|
|
21
|
+
* 그래서 값은 **그대로 지나간다** — 코드로 바꾸지도, 라벨로 되돌리지도 않는다. 이것을 안
|
|
22
|
+
* 가르면 저장할 때 `wordAttributeValueCodeOf` 가 `null` 을 내어 **고른 값이 통째로
|
|
23
|
+
* 버려지고**, 되읽을 때도 사전에 없어 사라진다. 둘 다 조용히 일어난다.
|
|
24
|
+
*/
|
|
25
|
+
const FREE_VALUE_SECTIONS = new Set(['field']);
|
|
14
26
|
/**
|
|
15
27
|
* **등급이 빈 단어를 가리키는 코드** — `'none'`((205), 사장님 지시 2026-10-12).
|
|
16
28
|
*
|
|
@@ -59,13 +71,14 @@ export function matchesGradeCodes(codes, grade) {
|
|
|
59
71
|
return set.has(GRADE_NONE);
|
|
60
72
|
return set.has(String(grade));
|
|
61
73
|
}
|
|
62
|
-
const sectionCodes = ['grade', 'syllables', 'pos', 'wordType'];
|
|
74
|
+
const sectionCodes = ['grade', 'syllables', 'pos', 'wordType', 'field'];
|
|
63
75
|
/** 등급 코드 — `'none'` 은 **등급이 빈 단어**다(값이 아니라 **빔**이다. GRADE_NONE 주석을 볼 것) */
|
|
64
76
|
const gradeValues = ['1', '2', '3', '4', 'none'];
|
|
65
77
|
const syllableValues = ['1', '2', '3', '4', '5+'];
|
|
66
78
|
const posValues = ['noun', 'verb', 'modifier'];
|
|
67
79
|
const wordTypeValues = ['native', 'sino', 'foreign', 'mixed'];
|
|
68
|
-
|
|
80
|
+
/* `field` 는 값이 자유라 셀 목록이 없다 — `FREE_VALUE_SECTIONS` 를 볼 것 */
|
|
81
|
+
const valuesBySection = { grade: gradeValues, syllables: syllableValues, pos: posValues, wordType: wordTypeValues, field: [] };
|
|
69
82
|
/** 품사 코드 → 사전 품사 약자(단어의 pos 에 들어 있는지 본다) · 어종 코드 → 사전 어종 값 */
|
|
70
83
|
export const POS_CODE_TARGETS = { noun: ['명', '수'], verb: ['동', '형'], modifier: ['부', '관'] };
|
|
71
84
|
export const WORD_TYPE_CODE_VALUES = { native: '고유', sino: '한자', foreign: '외래', mixed: '혼종' };
|
|
@@ -91,6 +104,7 @@ function dictFor(lng) {
|
|
|
91
104
|
syllables: t('wordSearch.filter.syllables', '음절 수'),
|
|
92
105
|
pos: t('wordSearch.filter.partOfSpeech', '품사'),
|
|
93
106
|
wordType: t('wordSearch.filter.wordType', '유형'),
|
|
107
|
+
field: t('wordSearch.filter.field', '의미범주'),
|
|
94
108
|
},
|
|
95
109
|
value: {
|
|
96
110
|
grade: {
|
|
@@ -113,6 +127,8 @@ function dictFor(lng) {
|
|
|
113
127
|
foreign: t('wordSearch.filter.type.foreign', '외래어'),
|
|
114
128
|
mixed: t('wordSearch.filter.type.mixed', '혼종어'),
|
|
115
129
|
},
|
|
130
|
+
/* 값이 자유로운 절 — 라벨이 곧 값이라 표가 비어 있다(`wordAttributeValueLabelOf` 가 코드를 그대로 돌려준다) */
|
|
131
|
+
field: {},
|
|
116
132
|
},
|
|
117
133
|
};
|
|
118
134
|
dictCache.set(lng, d);
|
|
@@ -136,6 +152,9 @@ export function wordAttributeSectionOf(label) {
|
|
|
136
152
|
}
|
|
137
153
|
/** 한 섹션의 값(코드·어느 언어 라벨·옛 라벨) → 값 코드. 못 읽으면 null. */
|
|
138
154
|
export function wordAttributeValueCodeOf(section, value) {
|
|
155
|
+
/* 값이 자유로운 절은 **그대로 지나간다** — 코드가 따로 없다(`FREE_VALUE_SECTIONS`) */
|
|
156
|
+
if (FREE_VALUE_SECTIONS.has(section))
|
|
157
|
+
return value;
|
|
139
158
|
if (valuesBySection[section].includes(value))
|
|
140
159
|
return value;
|
|
141
160
|
for (const lng of languages()) {
|
|
@@ -191,7 +210,8 @@ export function wordAttributeFilterFromCodes(codes, lng = i18n.language) {
|
|
|
191
210
|
const d = dictFor(lng || 'ko');
|
|
192
211
|
const out = {};
|
|
193
212
|
for (const s of sectionCodes) {
|
|
194
|
-
|
|
213
|
+
/* 자유 절은 값이 곧 라벨이다 — 사전에 없다고 버리면 저장했다 되읽을 때 사라진다 */
|
|
214
|
+
const labels = (codes?.[s] ?? []).map((c) => d.value[s][c] ?? (FREE_VALUE_SECTIONS.has(s) ? c : undefined)).filter((x) => !!x);
|
|
195
215
|
if (labels.length)
|
|
196
216
|
out[d.section[s]] = labels;
|
|
197
217
|
}
|