@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.
@@ -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.323.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('calendar.yearMonth', { year, month: 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
+ 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('calendar.yearMonth', { year, month, defaultValue: '{{year}}년 {{month}}월' }), min, max);
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('calendar.yearMonth', { year: o.year, month: o.month });
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
- : wordAttributeSectionOf(label);
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
- const fields = filterSelected['의미범주'];
197
- if (fields?.length) {
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
- const valuesBySection = { grade: gradeValues, syllables: syllableValues, pos: posValues, wordType: wordTypeValues };
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
- const labels = (codes?.[s] ?? []).map((c) => d.value[s][c]).filter((x) => !!x);
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
  }
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.323.0";
49
+ export declare const CORE_VERSION = "0.325.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.323.0';
49
+ export const CORE_VERSION = '0.325.0';