@ihabdevteam/core 0.206.0 → 0.208.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.
@@ -1626,7 +1626,7 @@ export declare function copyText(text: string): Promise<boolean>;
1626
1626
  *
1627
1627
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
1628
1628
  */
1629
- export declare const CORE_VERSION = "0.206.0";
1629
+ export declare const CORE_VERSION = "0.208.0";
1630
1630
 
1631
1631
  /** @public */
1632
1632
  declare const coreDefault: Record<string, unknown>;
@@ -2379,7 +2379,12 @@ export declare interface FilterPanelProps {
2379
2379
  * CSS 로 오른쪽 끝까지 미는 억지를 썼다(말귀 단어표 만들기 2026-09-29).
2380
2380
  * **펼치면 그 자리를 `headerAction` 이 쓴다** — 둘은 같은 칸을 번갈아 쓴다.
2381
2381
  *
2382
- * 요약 **글월을 짓는 셈은 부르는 쪽** 에 있다. 코어는 자리만 낸다 —
2382
+ * **안 주면 코어가 짓는다** — `절 이름 + 고른 값` 을 ` · ` 으로 이어 한 줄로 만든다
2383
+ * (`등급 전체 · 음절 수 2음절`). 이름표도 값도 코어 것이라 부르는 쪽이 옮겨 적으면 두 벌이 되고,
2384
+ * 표 설정 판처럼 값이 `WordTableBlock` 안에 있는 자리는 부르는 쪽이 지을 길조차 없다
2385
+ * (말귀 요청 ㉗, 2026-09-29). 셈은 `utils/filterPanelSummary` 에 있다.
2386
+ *
2387
+ * **주면 준 것이 이긴다** — 글월을 부르는 쪽이 쥐고 싶을 때 쓴다. 코어는 자리만 낸다 —
2383
2388
  * 여러 조각을 넣을 때는 묶음에 `role="group"` 과 한 줄짜리 `aria-label` 을 주고 낱개는 `aria-hidden` 으로
2384
2389
  * 숨기는 편이 낫다(낭독기가 조각마다 끊어 읽지 않게). */
2385
2390
  headerSummary?: ReactNode;
@@ -10276,7 +10281,7 @@ export declare interface WordSearchFilterPanelProps {
10276
10281
  }
10277
10282
 
10278
10283
  /** @public */
10279
- export declare function WordSearchResultBlock({ titleIcon, resultCount, resultUnit, searchQuery, items, groups, selected, onSelect, maxSelect, onClearSelection, onPrint, onPrintEmpty, className, emptyHint, sectionType, disc, notice, }: WordSearchResultBlockProps): JSX.Element;
10284
+ export declare function WordSearchResultBlock({ titleIcon, resultCount, resultUnit, searchQuery, items, groups, selected, onSelect, maxSelect, onClearSelection, onPrint, onPrintEmpty, className, emptyHint, sectionType, disc, }: WordSearchResultBlockProps): JSX.Element;
10280
10285
 
10281
10286
  /** @public */
10282
10287
  export declare interface WordSearchResultBlockProps {
@@ -10303,14 +10308,6 @@ export declare interface WordSearchResultBlockProps {
10303
10308
  sectionType?: 'page' | 'pagePopup' | 'popup';
10304
10309
  /** 제목 아래 부제(설명). 지정 시 기본 disc(선택 안내)를 대체한다. */
10305
10310
  disc?: ReactNode;
10306
- /**
10307
- * 기본 선택 안내 **아래에 덧붙는** 한 줄 — 지금 결과가 걸러지고 있다는 알림 등.
10308
- *
10309
- * `disc` 와 달리 기본 안내를 밀어내지 않는다. 밀어내면 '12개까지 선택할 수 있습니다
10310
- * (n/12)' 가 사라지는데, 그것이 몇 칸을 썼는지 보이는 유일한 자리다 — 필터를 켠
10311
- * 사람에게서 하필 그 숫자를 뺏게 된다.
10312
- */
10313
- notice?: ReactNode;
10314
10311
  /**
10315
10312
  * 결과가 없을 때 "검색 결과가 없습니다." 아래에 덧붙일 한 줄.
10316
10313
  *
@@ -13,6 +13,9 @@
13
13
  display: flex;
14
14
  align-items: center;
15
15
  justify-content: space-between;
16
+ /* 제목과 요약이 맞닿지 않게 — 요약이 길면 `space-between` 의 빈 곳이 0이 되어
17
+ '표 설정구성 이미지…' 로 붙어 읽힌다(실측 2026-09-29). */
18
+ gap: var(--spacing-s);
16
19
  }
17
20
 
18
21
  /* ── 접기 가능한 패널 ─────────────────────────────────────────
@@ -54,6 +57,10 @@
54
57
  .filter-panel__title {
55
58
  display: block;
56
59
  text-align: left;
60
+ /* 안 접힌다 — 넘치면 …로 자른다(title-row 의 `max-width` 에 닿았을 때) */
61
+ white-space: nowrap;
62
+ overflow: hidden;
63
+ text-overflow: ellipsis;
57
64
  }
58
65
 
59
66
  /* 헤더 좌측: 접기 버튼 + 타이틀 인라인 */
@@ -62,12 +69,29 @@
62
69
  align-items: center;
63
70
  gap: var(--spacing-xs);
64
71
  min-width: 0;
72
+ /* **제목이 먼저 자리를 얻고 요약이 양보한다.** 접힌 판의 요약은 길어서(표 설정은 절이 일곱이다)
73
+ 둘이 같이 줄면 짧은 제목이 두 줄로 접혀 60px 머리에 눌린다(실측 2026-09-29: '표 설정' 52px).
74
+ 그렇다고 제목이 머리를 다 먹으면 요약이 통째로 사라지므로 절반까지만 준다. */
75
+ flex-shrink: 0;
76
+ max-width: 50%;
77
+ }
78
+
79
+
80
+ /* 접힌 판이 제 속을 말하는 한 줄 — 길어지면 줄이지 말고 …로 자른다(머리는 한 줄 높이다).
81
+ `min-width: 0` 이 없으면 flex 칸이 제 속만큼 벌어져 제목을 밀어낸다. */
82
+ .filter-panel__header-summary {
83
+ color: var(--color-gray);
84
+ overflow: hidden;
85
+ text-overflow: ellipsis;
86
+ white-space: nowrap;
87
+ min-width: 0;
65
88
  }
66
89
 
67
90
  .filter-panel__header-actions {
68
91
  display: flex;
69
92
  align-items: center;
70
93
  gap: var(--spacing-xs);
94
+ min-width: 0;
71
95
  margin-left: auto;
72
96
  }
73
97
 
@@ -14,7 +14,12 @@ export interface FilterPanelProps {
14
14
  * CSS 로 오른쪽 끝까지 미는 억지를 썼다(말귀 단어표 만들기 2026-09-29).
15
15
  * **펼치면 그 자리를 `headerAction` 이 쓴다** — 둘은 같은 칸을 번갈아 쓴다.
16
16
  *
17
- * 요약 **글월을 짓는 셈은 부르는 쪽** 에 있다. 코어는 자리만 낸다 —
17
+ * **안 주면 코어가 짓는다** — `절 이름 + 고른 값` 을 ` · ` 으로 이어 한 줄로 만든다
18
+ * (`등급 전체 · 음절 수 2음절`). 이름표도 값도 코어 것이라 부르는 쪽이 옮겨 적으면 두 벌이 되고,
19
+ * 표 설정 판처럼 값이 `WordTableBlock` 안에 있는 자리는 부르는 쪽이 지을 길조차 없다
20
+ * (말귀 요청 ㉗, 2026-09-29). 셈은 `utils/filterPanelSummary` 에 있다.
21
+ *
22
+ * **주면 준 것이 이긴다** — 글월을 부르는 쪽이 쥐고 싶을 때 쓴다. 코어는 자리만 낸다 —
18
23
  * 여러 조각을 넣을 때는 묶음에 `role="group"` 과 한 줄짜리 `aria-label` 을 주고 낱개는 `aria-hidden` 으로
19
24
  * 숨기는 편이 낫다(낭독기가 조각마다 끊어 읽지 않게). */
20
25
  headerSummary?: ReactNode;
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { cx } from '../utils/cx';
4
- import { Fragment, useState } from 'react';
4
+ import { Fragment, useMemo, useState } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import './FilterPanel.css';
7
7
  import Button from './Button';
@@ -12,6 +12,7 @@ export { FILTER_ALL_VALUE } from '../types/filterPanel';
12
12
  import { FILTER_ALL_VALUE } from '../types/filterPanel';
13
13
  import FilterSectionInfo from './FilterSectionInfo';
14
14
  import { devWarnOnce } from '../utils/devWarn';
15
+ import { buildFilterSummary } from '../utils/filterPanelSummary';
15
16
  /** @public */
16
17
  export default function FilterPanel({ title, headerAction, headerSummary, collapsible = false, defaultCollapsed = false, collapsed: controlledCollapsed, onCollapsedChange, footer, sections, selected = {}, onSelect, align, sectionAlign, className, }) {
17
18
  // align 우선. 없으면 구 prop(sectionAlign) 매핑, 그것도 없으면 inline 기본.
@@ -39,13 +40,19 @@ export default function FilterPanel({ title, headerAction, headerSummary, collap
39
40
  return;
40
41
  toggleCollapsed();
41
42
  };
43
+ /* 안 주면 **코어가 짓는다** — 이름표도 값도 코어 것이라 부르는 쪽이 옮겨 적으면 두 벌이 되고,
44
+ 표 설정 판처럼 값이 아예 `WordTableBlock` 안에 있는 자리는 지을 길조차 없다(말귀 요청 ㉗).
45
+ `headerSummary` 를 주면 그것이 이긴다 — 글월을 부르는 쪽이 쥐던 길은 그대로 열려 있다.
46
+ 접혀 있을 때만 쓰이므로 펼친 동안은 셈하지 않는다. */
47
+ const 기본요약 = useMemo(() => (headerSummary !== undefined || !collapsed ? '' : buildFilterSummary(sections, selected, t('common.all', '전체'))), [headerSummary, collapsed, sections, selected, t]);
48
+ const summaryNode = headerSummary ?? (기본요약 ? _jsx("span", { className: "filter-panel__header-summary body-3", children: 기본요약 }) : null);
42
49
  const showHeader = title || headerAction || headerSummary || collapsible;
43
- return (_jsxs("div", { className: cx('filter-panel', collapsible ? 'filter-panel--collapsible' : '', collapsed ? 'filter-panel--collapsed' : '', className), children: [showHeader && (_jsxs("div", { className: "filter-panel__header", onClick: collapsible ? 머리누름 : undefined, children: [_jsxs("div", { className: "filter-panel__title-row", children: [collapsible && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-down' : 'chevron-up',
50
+ return (_jsxs("div", { className: cx('filter-panel', collapsible ? 'filter-panel--collapsible' : '', collapsed ? 'filter-panel--collapsed' : '', className), children: [showHeader && (_jsxs("div", { className: "filter-panel__header", onClick: collapsible ? 머리누름 : undefined, children: [_jsx("div", { className: "filter-panel__title-row", children: title && _jsx("span", { className: "filter-panel__title h4", children: title }) }), _jsxs("div", { className: "filter-panel__header-actions", children: [collapsed ? summaryNode : headerAction, collapsible && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-down' : 'chevron-up',
44
51
  /* Button은 aria-label을 자기 ariaLabel prop으로 덮어쓴다({...rest} 뒤에
45
52
  aria-label={ariaLabelString}을 다시 쓴다) — kebab으로 넘기면 undefined가
46
53
  되어 이름이 통째로 사라진다. 저장소의 다른 호출부는 모두 ariaLabel을 쓴다.
47
54
  문구도 이 파일만 한국어로 박혀 있었다(키는 이미 있다). */
48
- className: "filter-panel__collapse-toggle", ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), "aria-expanded": !collapsed, onClick: toggleCollapsed })), title && _jsx("span", { className: "filter-panel__title h4", children: title })] }), _jsx("div", { className: "filter-panel__header-actions", children: collapsed ? headerSummary : headerAction })] })), !collapsed && (_jsxs(_Fragment, { children: [_jsx("div", { className: cx('filter-panel__sections', isRow ? 'filter-panel__sections--row' : ''), children: sections.map(section => {
55
+ className: "filter-panel__collapse-toggle", ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), "aria-expanded": !collapsed, onClick: toggleCollapsed }))] })] })), !collapsed && (_jsxs(_Fragment, { children: [_jsx("div", { className: cx('filter-panel__sections', isRow ? 'filter-panel__sections--row' : ''), children: sections.map(section => {
49
56
  const sel = selected[section.label];
50
57
  const isAllSelected = sel === undefined || (Array.isArray(sel) ? sel.length === 0 : false);
51
58
  // section.items에 빈 항목(null/undefined)이 섞여 들어오면 item.value 접근에서
@@ -33,7 +33,11 @@
33
33
  display: flex;
34
34
  align-items: center;
35
35
  flex-wrap: wrap;
36
- gap: var(--spacing-xs);
36
+ /* 가로로만 띄운다 — 아래 줄(안내·알림)과의 세로 틈은 **제목의 줄높이가 이미 낸다.**
37
+ 거기에 8px 을 더 얹으니 제목 아래가 13px 로 벌어져 넓어 보였다(사용자 지시 2026-09-29).
38
+ 제목이 h3 가 되며(㉘) 줄높이가 26 → 30px 로 늘어 그만큼 더 떴다. */
39
+ column-gap: var(--spacing-xs);
40
+ row-gap: 0;
37
41
  }
38
42
  .word-search-result-block__head-main {
39
43
  display: flex;
@@ -161,9 +165,4 @@
161
165
 
162
166
  /* 부제가 두 줄이 될 때 — 알림(걸러지고 있다) 위, 선택 안내 아래로 쌓는다.
163
167
  Section 의 disc 는 한 줄을 전제하므로 여기서 블록으로 만든다. */
164
- .word-search-result-block__notice,
165
- .word-search-result-block__disc { display: block; }
166
168
 
167
- /* 알림은 왜 결과가 적은지 말하는 줄이라 먼저 읽히게 색을 준다.
168
- --color-gray 는 부제 기본색이고, 알림만 본문색으로 올린다. */
169
- .word-search-result-block__notice { color: var(--color-text-primary); }
@@ -32,14 +32,6 @@ export interface WordSearchResultBlockProps {
32
32
  sectionType?: 'page' | 'pagePopup' | 'popup';
33
33
  /** 제목 아래 부제(설명). 지정 시 기본 disc(선택 안내)를 대체한다. */
34
34
  disc?: ReactNode;
35
- /**
36
- * 기본 선택 안내 **아래에 덧붙는** 한 줄 — 지금 결과가 걸러지고 있다는 알림 등.
37
- *
38
- * `disc` 와 달리 기본 안내를 밀어내지 않는다. 밀어내면 '12개까지 선택할 수 있습니다
39
- * (n/12)' 가 사라지는데, 그것이 몇 칸을 썼는지 보이는 유일한 자리다 — 필터를 켠
40
- * 사람에게서 하필 그 숫자를 뺏게 된다.
41
- */
42
- notice?: ReactNode;
43
35
  /**
44
36
  * 결과가 없을 때 "검색 결과가 없습니다." 아래에 덧붙일 한 줄.
45
37
  *
@@ -50,4 +42,4 @@ export interface WordSearchResultBlockProps {
50
42
  emptyHint?: ReactNode;
51
43
  }
52
44
  /** @public */
53
- export default function WordSearchResultBlock({ titleIcon, resultCount, resultUnit, searchQuery, items, groups, selected, onSelect, maxSelect, onClearSelection, onPrint, onPrintEmpty, className, emptyHint, sectionType, disc, notice, }: WordSearchResultBlockProps): import("react").JSX.Element;
45
+ export default function WordSearchResultBlock({ titleIcon, resultCount, resultUnit, searchQuery, items, groups, selected, onSelect, maxSelect, onClearSelection, onPrint, onPrintEmpty, className, emptyHint, sectionType, disc, }: WordSearchResultBlockProps): import("react").JSX.Element;
@@ -16,7 +16,7 @@ import { makeWordQueryMatcher } from '../../utils/koreanSyllable';
16
16
  const 가나다 = new Intl.Collator('ko');
17
17
  import { withIga, withEulReul } from '../../utils/josa';
18
18
  /** @public */
19
- export default function WordSearchResultBlock({ titleIcon, resultCount, resultUnit, searchQuery, items, groups, selected = [], onSelect, maxSelect, onClearSelection, onPrint, onPrintEmpty, className, emptyHint, sectionType = 'page', disc, notice, }) {
19
+ export default function WordSearchResultBlock({ titleIcon, resultCount, resultUnit, searchQuery, items, groups, selected = [], onSelect, maxSelect, onClearSelection, onPrint, onPrintEmpty, className, emptyHint, sectionType = 'page', disc, }) {
20
20
  const { t, i18n } = useTranslation();
21
21
  // 한국어에서만 조사(이/가·을/를)를 unit 말미에 붙인다 — 문구에는 조사를 고정하지 않는다.
22
22
  const ko = i18n.language?.startsWith('ko') ?? false;
@@ -154,16 +154,28 @@ export default function WordSearchResultBlock({ titleIcon, resultCount, resultUn
154
154
  const discLabel = resultCount !== undefined && resultCount > 0 && maxSelect !== undefined && resultUnit
155
155
  ? t('wordSearch.resultDisc', { unit: ko ? withEulReul(resultUnit) : resultUnit, max: maxSelect, selected: selected.length })
156
156
  : undefined;
157
- /* 기본 안내(있으면) 위에 알림 줄을 얹는다 — 알림이 먼저 눈에 들어오고, 선택 안내는
158
- 제자리에 남는다. 둘 다 없으면 undefined 라 부제 자체가 안 그려진다. */
159
- const discNode = disc ?? (notice
160
- ? _jsxs(_Fragment, { children: [_jsx("span", { className: "word-search-result-block__notice", children: notice }), discLabel ? _jsx("span", { className: "word-search-result-block__disc", children: discLabel }) : null] })
161
- : discLabel);
157
+ /* 부제 자리는 **하나**다 — `disc` 를 주면 기본 안내를 대신하고, 안 주면 기본 안내가
158
+ 선다. 둘 다 없으면 undefined 라 부제 자체가 안 그려진다.
159
+
160
+ 한때 `notice` 로 이 위에 한 줄을 더 얹을 수 있었으나 머리가 세 줄이 되어 무엇을
161
+ 먼저 읽을지 안 보여 걷었다(사용자 지시 2026-09-29).
162
+
163
+ **걸러지고 있다는 말을 `disc` 로 옮기지 마라.** `disc` 는 기본 안내를 갈아 끼우므로
164
+ `12개까지 선택할 수 있습니다 (n/12)` 가 사라진다 — 몇 칸을 썼는지 보이는 유일한
165
+ 자리다. 2026-08-28 에 정확히 그 일이 있었고 `notice` 가 그 때문에 생겼다. 지금은
166
+ 그 알림을 **안 보이는 쪽**을 골랐다(위 지시). 다시 필요해지면 `disc` 가 아니라
167
+ 안내 줄 곁에 설 자리를 새로 내야 한다. */
168
+ const discNode = disc ?? discLabel;
162
169
  return (
163
170
  /* 제목·부제·조작부가 **상자 안** 에 선다(사용자 지시 2026-09-29) — 전에는 `Section` 의 머리가
164
171
  상자 밖에 서서 제목이 테두리 위에 뜨고 좌우 여백도 옆의 `FilterPanel` 과 달라 보였다.
165
- 이제 여백(--spacing-l)·제목 등급(h4)·부제(body-3 회색)를 그 판과 맞춘다. */
166
- _jsxs(Section, { type: sectionType, className: cx('word-search-result-block', className), children: [_jsxs("div", { className: "word-search-result-block__inner", children: [(titleLabel || toolbar || discNode) && (_jsxs("div", { className: "word-search-result-block__head", children: [_jsxs("div", { className: "word-search-result-block__head-main", children: [titleIcon && _jsx(Icon, { icon: titleIcon, className: "word-search-result-block__head-icon" }), titleLabel && _jsx("span", { className: "word-search-result-block__title h4", children: titleLabel })] }), toolbar && _jsx("div", { className: "word-search-result-block__head-actions", children: toolbar }), discNode && _jsx("div", { className: "word-search-result-block__head-disc body-3", children: discNode })] })), (groups ? groups.length === 0 : items.length === 0) && (_jsxs("div", { className: "word-search-result-block__empty-wrap", children: [_jsx("p", { className: "word-search-result-block__empty body-3", children: t('wordSearch.emptyResult', '검색 결과가 없습니다.') }), emptyHint && _jsx("p", { className: "word-search-result-block__empty-hint body-3", children: emptyHint })] })), groups ? ((pagedGroups ?? []).map((group) => (_jsx("div", { className: "word-search-result-block__group", children: _jsx("div", { className: "word-search-result-block__items", children: group.items.map((item) => {
172
+ 이제 여백(--spacing-l)·부제(body-3 회색)를 그 판과 맞춘다.
173
+
174
+ **제목만 한 급 위(h3)다**(말귀 요청 ㉘, 2026-09-29). 0.206.0 에서는 곁의 `FilterPanel`
175
+ 과 아주 같은 h4 로 맞췄는데, 그러니 결과 덩이의 머리가 그 위 판 제목들(`검색 옵션` ·
176
+ `단어 검색 설정`)과 같은 급이 되어 **무엇이 무엇의 머리인지 안 보였다.** 거르개 판은
177
+ 결과를 고르는 도구고 이 덩이는 그 결과라, 나란한 것이 아니라 아래 딸린 것이다. */
178
+ _jsxs(Section, { type: sectionType, className: cx('word-search-result-block', className), children: [_jsxs("div", { className: "word-search-result-block__inner", children: [(titleLabel || toolbar || discNode) && (_jsxs("div", { className: "word-search-result-block__head", children: [_jsxs("div", { className: "word-search-result-block__head-main", children: [titleIcon && _jsx(Icon, { icon: titleIcon, className: "word-search-result-block__head-icon" }), titleLabel && _jsx("span", { className: "word-search-result-block__title h3", children: titleLabel })] }), toolbar && _jsx("div", { className: "word-search-result-block__head-actions", children: toolbar }), discNode && _jsx("div", { className: "word-search-result-block__head-disc body-3", children: discNode })] })), (groups ? groups.length === 0 : items.length === 0) && (_jsxs("div", { className: "word-search-result-block__empty-wrap", children: [_jsx("p", { className: "word-search-result-block__empty body-3", children: t('wordSearch.emptyResult', '검색 결과가 없습니다.') }), emptyHint && _jsx("p", { className: "word-search-result-block__empty-hint body-3", children: emptyHint })] })), groups ? ((pagedGroups ?? []).map((group) => (_jsx("div", { className: "word-search-result-block__group", children: _jsx("div", { className: "word-search-result-block__items", children: group.items.map((item) => {
167
179
  const isSelected = selected.includes(item.id);
168
180
  return (_jsx("div", { onMouseEnter: e => {
169
181
  openHover(e.currentTarget);
@@ -0,0 +1,14 @@
1
+ import type { FilterPanelSection } from '../types/filterPanel';
2
+ /**
3
+ * **접힌 거르개 판이 제 속을 한 줄로 말하는 글월**을 절과 고른 값에서 짓는다
4
+ * (`등급 전체 · 음절 수 2음절 · 품사 전체`).
5
+ *
6
+ * 왜 코어가 짓나: 이름표도 값도 코어 것이라 **부르는 쪽이 옮겨 적으면 두 벌이 된다.**
7
+ * 말귀가 등급·음절 수 이름표를 두 화면에 다르게 적어 한 번 어긋났고(말귀 요청 ㉗,
8
+ * 2026-09-29), 표 설정 판은 아예 값이 `WordTableBlock` 안에 있어 부르는 쪽이 글월을
9
+ * 지을 길조차 없었다. 그래서 **기본값은 코어가 짓고, 덮고 싶을 때만 `headerSummary`** 다.
10
+ *
11
+ * 안 담는 절: `note`(고르는 절이 아니다) · `input`(빈 칸을 '전체'라 말하면 거짓이다) ·
12
+ * 고른 것이 없는데 '전체' 보기도 없는 절(말할 것이 없다).
13
+ */
14
+ export declare function buildFilterSummary(sections: readonly FilterPanelSection[], selected: Record<string, string | string[]> | undefined, 전체말: string): string;
@@ -0,0 +1,57 @@
1
+ import { FILTER_ALL_VALUE } from '../types/filterPanel';
2
+ /**
3
+ * **접힌 거르개 판이 제 속을 한 줄로 말하는 글월**을 절과 고른 값에서 짓는다
4
+ * (`등급 전체 · 음절 수 2음절 · 품사 전체`).
5
+ *
6
+ * 왜 코어가 짓나: 이름표도 값도 코어 것이라 **부르는 쪽이 옮겨 적으면 두 벌이 된다.**
7
+ * 말귀가 등급·음절 수 이름표를 두 화면에 다르게 적어 한 번 어긋났고(말귀 요청 ㉗,
8
+ * 2026-09-29), 표 설정 판은 아예 값이 `WordTableBlock` 안에 있어 부르는 쪽이 글월을
9
+ * 지을 길조차 없었다. 그래서 **기본값은 코어가 짓고, 덮고 싶을 때만 `headerSummary`** 다.
10
+ *
11
+ * 안 담는 절: `note`(고르는 절이 아니다) · `input`(빈 칸을 '전체'라 말하면 거짓이다) ·
12
+ * 고른 것이 없는데 '전체' 보기도 없는 절(말할 것이 없다).
13
+ */
14
+ export function buildFilterSummary(sections, selected, 전체말) {
15
+ const 조각들 = [];
16
+ for (const section of sections ?? []) {
17
+ if (section.type === 'note' || section.type === 'input')
18
+ continue;
19
+ const 이름 = section.displayLabel ?? section.label;
20
+ const 값 = 값말(section, selected?.[section.label], 전체말);
21
+ if (값 === null)
22
+ continue;
23
+ 조각들.push(`${이름} ${값}`);
24
+ }
25
+ return 조각들.join(' · ');
26
+ }
27
+ /** 한 절이 무엇으로 정해져 있는지 — 말할 것이 없으면 `null` */
28
+ function 값말(section, sel, 전체말) {
29
+ const 고른것 = Array.isArray(sel) ? sel : sel === undefined ? [] : [sel];
30
+ /* '전체' 는 두 꼴로 온다 — 아무것도 안 고른 것과 '전체' 보기를 고른 것. 같은 말로 읽는다. */
31
+ const 실제 = 고른것.filter(v => v !== FILTER_ALL_VALUE);
32
+ if (실제.length === 0) {
33
+ return section.allOption ? (section.allLabel ?? 전체말) : null;
34
+ }
35
+ return 실제.map(v => 보기말(section, v)).join(', ');
36
+ }
37
+ /** 고른 값 하나를 사람이 읽는 말로 — 보기 목록에 없으면 값 그대로 */
38
+ function 보기말(section, value) {
39
+ const 보기들 = [
40
+ ...(section.items ?? []),
41
+ ...(section.groups ?? []).flat(),
42
+ ];
43
+ for (const item of 보기들) {
44
+ if (item == null)
45
+ continue;
46
+ if (typeof item === 'string') {
47
+ if (item === value)
48
+ return item;
49
+ continue;
50
+ }
51
+ if (item.value !== value)
52
+ continue;
53
+ /* 단추에 쓰는 차례 그대로 — `labels` 는 여러 줄로 쪼갠 것이라 한 줄로 이어 붙인다 */
54
+ return item.displayLabel ?? item.labels?.join(' ') ?? item.value;
55
+ }
56
+ return value;
57
+ }
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.206.0";
49
+ export declare const CORE_VERSION = "0.208.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.206.0';
49
+ export const CORE_VERSION = '0.208.0';