@ihabdevteam/core 0.215.0 → 0.217.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.215.0";
1629
+ export declare const CORE_VERSION = "0.217.0";
1630
1630
 
1631
1631
  /** @public */
1632
1632
  declare const coreDefault: Record<string, unknown>;
@@ -10373,7 +10373,7 @@ export declare interface WordSearchResultWord {
10373
10373
  */
10374
10374
  export declare function WordTable({ title, titleIcon, disc, className, orientation, contentTypes, cutLine, showOutline, showDivLine, items, editCells, count, activeCell, onActivateCell, onRemoveCell, onRemoveItem, onReorder, renderCellAction, showFillerAdd, contentRef, buttonSize, brand, code, pageNumber, pageNumberPosition, fitHeight, scoring, marks, onMarkClick, scoreIndexOffset, }: WordTableProps): JSX.Element;
10375
10375
 
10376
- export declare function WordTableBlock({ title, titleIcon, disc, filterTitle, filterCollapsible, filterDefaultCollapsed, filterCollapsed, onFilterCollapsedChange, filterHeaderSummary, items, onSaveComplete, onLoad, onRemoveItem, onReset, resetLabel, saveButtonLabel, secondarySaveButtonLabel, onReorder, showCellCount, onAddItem, className, renderSavePopup, renderWordSearch, hideControls, saveOnly, tableName, nameValue, onNameChange, descriptionValue, onDescriptionChange, namePlaceholder, descriptionPlaceholder, existingNames, editingName, onCancel, initialItemsPerPage, renderCellAction, actionsLeftExtra }: WordTableBlockProps): JSX.Element;
10376
+ export declare function WordTableBlock({ title, titleIcon, disc, filterTitle, filterCollapsible, filterDefaultCollapsed, filterCollapsed, onFilterCollapsedChange, filterHeaderSummary, brand, code, items, onSaveComplete, onLoad, onRemoveItem, onReset, resetLabel, saveButtonLabel, secondarySaveButtonLabel, onReorder, showCellCount, onAddItem, className, renderSavePopup, renderWordSearch, hideControls, saveOnly, tableName, nameValue, onNameChange, descriptionValue, onDescriptionChange, namePlaceholder, descriptionPlaceholder, existingNames, editingName, onCancel, initialItemsPerPage, renderCellAction, actionsLeftExtra }: WordTableBlockProps): JSX.Element;
10377
10377
 
10378
10378
  /** @public */
10379
10379
  export declare interface WordTableBlockProps {
@@ -10410,6 +10410,16 @@ export declare interface WordTableBlockProps {
10410
10410
  onFilterCollapsedChange?: (collapsed: boolean) => void;
10411
10411
  /** 그 판이 접혀도 머리 오른쪽에 남는 자리 — 요약. 안 주면 코어가 짓는다(`FilterPanel`). */
10412
10412
  filterHeaderSummary?: ReactNode;
10413
+ /**
10414
+ * 종이 머리·바닥 띠 — 안쪽 `WordTable` 로 그대로 간다.
10415
+ *
10416
+ * **`show` 가 켜지면 표 설정에 `상단 정보`·`하단 정보` 고르개가 저절로 선다** — 스위치를
10417
+ * 둘로 나누면 부르는 쪽이 어긋나게 줄 수 있어서다(말귀 요청 ㉛, 2026-09-30). 그 고르개로
10418
+ * 켜고 끈 값은 `top`·`bottom` 을 **덮어쓴다** — 화면에서 사람이 고른 것이 마지막 말이다.
10419
+ */
10420
+ brand?: WordTableProps['brand'];
10421
+ /** 표 번호 — 띠 위 오른쪽에 선다. 안쪽 `WordTable` 로 그대로 간다. */
10422
+ code?: string;
10413
10423
  disc?: ReactNode;
10414
10424
  /** 현재 표의 저장된 이름(불러온 표 등). 있으면 인쇄/PDF 저장 시 파일명으로 사용, 없으면 "오늘 날짜 + 단어표/대립쌍표" */
10415
10425
  tableName?: string;
@@ -10622,7 +10632,7 @@ export declare type WordTableOrientation = 'landscape' | 'portrait';
10622
10632
  * 표 내부에는 제목을 두지 않아 팝업 헤더 제목과 중복되지 않는다.
10623
10633
  * @public
10624
10634
  */
10625
- export declare function WordTablePopup({ items, words, title, subtitle, emptyText, buttons, renderButtons, renderFooter, footerClassName, footerLayout, printFileName, showSettings, autoPrint, className, onClose, scoring, onScoreChange, printHeader, beforeClose }: WordTablePopupProps): JSX.Element;
10635
+ export declare function WordTablePopup({ items, words, title, subtitle, emptyText, buttons, renderButtons, renderFooter, footerClassName, footerLayout, printFileName, showSettings, autoPrint, className, onClose, scoring, onScoreChange, printHeader, brand, code, beforeClose }: WordTablePopupProps): JSX.Element;
10626
10636
 
10627
10637
  /** @public */
10628
10638
  export declare interface WordTablePopupActions {
@@ -10669,6 +10679,14 @@ export declare interface WordTablePopupProps {
10669
10679
  /** 종이 맨 위에 찍을 한 줄(누구의·언제·무슨 표). 인쇄되는 것은 격자뿐이라
10670
10680
  * 이것이 없으면 뽑아 둔 종이에 아무 단서도 안 남는다. */
10671
10681
  printHeader?: string;
10682
+ /**
10683
+ * 종이 머리·바닥 띠 — 안쪽 `WordTable` 의 같은 이름 프롭으로 그대로 간다.
10684
+ * **사람이 실제로 인쇄하는 화면은 이 창을 지난다**(미리보기 · 채점지 · 고객 상세 ·
10685
+ * 훈련 더보기) — 여기가 안 흘리면 앱에서 띠를 켤 길이 없다(말귀 요청 ㉛, 2026-09-30).
10686
+ */
10687
+ brand?: WordTableProps['brand'];
10688
+ /** 표 번호 — 띠 위 오른쪽에 선다. 안쪽 `WordTable` 로 그대로 간다. */
10689
+ code?: string;
10672
10690
  /** 닫기 직전에 불린다. false 를 돌려주면 닫지 않는다(채점을 버릴지 묻는 용). */
10673
10691
  beforeClose?: PopupProps['beforeClose'];
10674
10692
  /** true면 팝업이 열리고 표가 렌더된 직후 인쇄를 1회 자동 시작한다(메뉴 '인쇄하기' 진입용). */
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type PopupProps } from '../Popup/Popup';
3
+ import type { WordTableProps } from '../WordTable';
3
4
  import type { WordSearchResultItem } from '../../types/wordSearch';
4
5
  import { type ScoreMark } from '../Word/wordTableScoring';
5
6
  import './WordTablePopup.css';
@@ -47,6 +48,14 @@ export interface WordTablePopupProps {
47
48
  /** 종이 맨 위에 찍을 한 줄(누구의·언제·무슨 표). 인쇄되는 것은 격자뿐이라
48
49
  * 이것이 없으면 뽑아 둔 종이에 아무 단서도 안 남는다. */
49
50
  printHeader?: string;
51
+ /**
52
+ * 종이 머리·바닥 띠 — 안쪽 `WordTable` 의 같은 이름 프롭으로 그대로 간다.
53
+ * **사람이 실제로 인쇄하는 화면은 이 창을 지난다**(미리보기 · 채점지 · 고객 상세 ·
54
+ * 훈련 더보기) — 여기가 안 흘리면 앱에서 띠를 켤 길이 없다(말귀 요청 ㉛, 2026-09-30).
55
+ */
56
+ brand?: WordTableProps['brand'];
57
+ /** 표 번호 — 띠 위 오른쪽에 선다. 안쪽 `WordTable` 로 그대로 간다. */
58
+ code?: string;
50
59
  /** 닫기 직전에 불린다. false 를 돌려주면 닫지 않는다(채점을 버릴지 묻는 용). */
51
60
  beforeClose?: PopupProps['beforeClose'];
52
61
  /** true면 팝업이 열리고 표가 렌더된 직후 인쇄를 1회 자동 시작한다(메뉴 '인쇄하기' 진입용). */
@@ -62,4 +71,4 @@ export interface WordTablePopupProps {
62
71
  * 표 내부에는 제목을 두지 않아 팝업 헤더 제목과 중복되지 않는다.
63
72
  * @public
64
73
  */
65
- export default function WordTablePopup({ items, words, title, subtitle, emptyText, buttons, renderButtons, renderFooter, footerClassName, footerLayout, printFileName, showSettings, autoPrint, className, onClose, scoring, onScoreChange, printHeader, beforeClose }: WordTablePopupProps): import("react").JSX.Element;
74
+ export default function WordTablePopup({ items, words, title, subtitle, emptyText, buttons, renderButtons, renderFooter, footerClassName, footerLayout, printFileName, showSettings, autoPrint, className, onClose, scoring, onScoreChange, printHeader, brand, code, beforeClose }: WordTablePopupProps): import("react").JSX.Element;
@@ -19,7 +19,7 @@ import { useToastState } from '../../hooks/useToastState';
19
19
  * 표 내부에는 제목을 두지 않아 팝업 헤더 제목과 중복되지 않는다.
20
20
  * @public
21
21
  */
22
- export default function WordTablePopup({ items, words, title, subtitle, emptyText, buttons, renderButtons, renderFooter, footerClassName, footerLayout, printFileName, showSettings, autoPrint, className, onClose, scoring = false, onScoreChange, printHeader, beforeClose }) {
22
+ export default function WordTablePopup({ items, words, title, subtitle, emptyText, buttons, renderButtons, renderFooter, footerClassName, footerLayout, printFileName, showSettings, autoPrint, className, onClose, scoring = false, onScoreChange, printHeader, brand, code, beforeClose }) {
23
23
  const { t } = useTranslation();
24
24
  // items 우선, 없으면 문자열 words → 한 단어=한 칸으로 매핑
25
25
  const tableItems = useMemo(() => {
@@ -30,7 +30,7 @@ export default function WordTablePopup({ items, words, title, subtitle, emptyTex
30
30
  .map((w, i) => ({ id: `${w}-${i}`, words: [{ label: w }] }));
31
31
  }, [items, words]);
32
32
  // 표 설정(구성·용지 방향·외곽선·페이지당 개수 등) — showSettings일 때만 FilterPanel로 노출하고, 값은 항상 표에 반영한다.
33
- const { settings, filterSections, filterSelected, handleSelect, pages, isPaginated, pageNumberPosition } = useWordTableDisplaySettings(tableItems);
33
+ const { settings, filterSections, filterSelected, handleSelect, pages, isPaginated, pageNumberPosition } = useWordTableDisplaySettings(tableItems, { paperBands: Boolean(brand?.show) });
34
34
  /* 쪽마다 첫 칸이 표 전체에서 몇 번째인지 — 쪽 안에서 0부터 다시 세는 번호를
35
35
  표 전체 번호로 옮긴다. */
36
36
  const pageStart = useMemo(() => pageStartIndexes(pages.map(p => p.length)), [pages]);
@@ -115,5 +115,5 @@ export default function WordTablePopup({ items, words, title, subtitle, emptyTex
115
115
  }, 100);
116
116
  return () => window.clearTimeout(id);
117
117
  }, [autoPrint]);
118
- return (_jsxs(Popup, { title: title ?? t('keyWords.title', '주요 단어'), subtitle: subtitle, size: "large", className: cx('word-table-popup', className), buttons: footerButtons, footer: customFooter, footerClassName: footerClassName, footerLayout: footerLayout, beforeClose: beforeClose, onClose: onClose, children: [tableItems.length > 0 ? (_jsxs("div", { className: "word-table-popup__body", children: [showSettings && (_jsx(TableFilterPanel, { sections: filterSections, selected: filterSelected, onSelect: handleSelect })), isPaginated ? (_jsx("div", { className: "word-table-popup__pages", children: pages.map((page, i) => (_jsx(WordTable, { items: page, orientation: settings.orientation, contentTypes: settings.contentTypes, cutLine: settings.cutLine, showOutline: settings.showOutline, showDivLine: settings.showDivLine, count: settings.count, contentRef: el => { pageRefs.current[i] = el; }, pageNumber: { current: i + 1, total: pages.length }, pageNumberPosition: pageNumberPosition, scoring: scoring, scoreIndexOffset: pageStart[i], marks: 표에줄채점, onMarkClick: handleMarkClick }, i))) })) : (_jsx("div", { className: "word-table-popup__table", children: _jsx(WordTable, { items: tableItems, orientation: settings.orientation, contentTypes: settings.contentTypes, cutLine: settings.cutLine, showOutline: settings.showOutline, showDivLine: settings.showDivLine, count: settings.count, contentRef: contentRef, fitHeight: true, scoring: scoring, marks: 표에줄채점, onMarkClick: handleMarkClick }) }))] })) : (_jsx("p", { className: "word-table-popup__empty body-3", children: emptyText ?? t('keyWords.empty', '추출된 주요 단어가 없습니다.') })), _jsx(Toast, { message: toastMsg, position: "top", onClose: handleToastClose }, toastKey)] }));
118
+ return (_jsxs(Popup, { title: title ?? t('keyWords.title', '주요 단어'), subtitle: subtitle, size: "large", className: cx('word-table-popup', className), buttons: footerButtons, footer: customFooter, footerClassName: footerClassName, footerLayout: footerLayout, beforeClose: beforeClose, onClose: onClose, children: [tableItems.length > 0 ? (_jsxs("div", { className: "word-table-popup__body", children: [showSettings && (_jsx(TableFilterPanel, { sections: filterSections, selected: filterSelected, onSelect: handleSelect })), isPaginated ? (_jsx("div", { className: "word-table-popup__pages", children: pages.map((page, i) => (_jsx(WordTable, { items: page, orientation: settings.orientation, contentTypes: settings.contentTypes, cutLine: settings.cutLine, showOutline: settings.showOutline, showDivLine: settings.showDivLine, count: settings.count, contentRef: el => { pageRefs.current[i] = el; }, pageNumber: { current: i + 1, total: pages.length }, pageNumberPosition: pageNumberPosition, brand: brand && { ...brand, top: settings.showTopInfo, bottom: settings.showBottomInfo }, code: code, scoring: scoring, scoreIndexOffset: pageStart[i], marks: 표에줄채점, onMarkClick: handleMarkClick }, i))) })) : (_jsx("div", { className: "word-table-popup__table", children: _jsx(WordTable, { items: tableItems, orientation: settings.orientation, contentTypes: settings.contentTypes, cutLine: settings.cutLine, showOutline: settings.showOutline, showDivLine: settings.showDivLine, count: settings.count, contentRef: contentRef, fitHeight: true, scoring: scoring, marks: 표에줄채점, onMarkClick: handleMarkClick, brand: brand && { ...brand, top: settings.showTopInfo, bottom: settings.showBottomInfo }, code: code }) }))] })) : (_jsx("p", { className: "word-table-popup__empty body-3", children: emptyText ?? t('keyWords.empty', '추출된 주요 단어가 없습니다.') })), _jsx(Toast, { message: toastMsg, position: "top", onClose: handleToastClose }, toastKey)] }));
119
119
  }
@@ -1,4 +1,4 @@
1
1
  import './WordTableBlock.css';
2
2
  import type { WordTableBlockProps } from './wordTableBlockTypes';
3
3
  export type { WordEntry, WordTableBlockProps } from './wordTableBlockTypes';
4
- export default function WordTableBlock({ title, titleIcon, disc, filterTitle, filterCollapsible, filterDefaultCollapsed, filterCollapsed, onFilterCollapsedChange, filterHeaderSummary, items, onSaveComplete, onLoad, onRemoveItem, onReset, resetLabel, saveButtonLabel, secondarySaveButtonLabel, onReorder, showCellCount, onAddItem, className, renderSavePopup, renderWordSearch, hideControls, saveOnly, tableName, nameValue, onNameChange, descriptionValue, onDescriptionChange, namePlaceholder, descriptionPlaceholder, existingNames, editingName, onCancel, initialItemsPerPage, renderCellAction, actionsLeftExtra }: WordTableBlockProps): import("react").JSX.Element;
4
+ export default function WordTableBlock({ title, titleIcon, disc, filterTitle, filterCollapsible, filterDefaultCollapsed, filterCollapsed, onFilterCollapsedChange, filterHeaderSummary, brand, code, items, onSaveComplete, onLoad, onRemoveItem, onReset, resetLabel, saveButtonLabel, secondarySaveButtonLabel, onReorder, showCellCount, onAddItem, className, renderSavePopup, renderWordSearch, hideControls, saveOnly, tableName, nameValue, onNameChange, descriptionValue, onDescriptionChange, namePlaceholder, descriptionPlaceholder, existingNames, editingName, onCancel, initialItemsPerPage, renderCellAction, actionsLeftExtra }: WordTableBlockProps): import("react").JSX.Element;
@@ -45,7 +45,7 @@ function 겹치지않는id(list) {
45
45
  return id === it.id ? it : { ...it, id };
46
46
  });
47
47
  }
48
- export default function WordTableBlock({ title, titleIcon, disc, filterTitle, filterCollapsible, filterDefaultCollapsed, filterCollapsed, onFilterCollapsedChange, filterHeaderSummary, items, onSaveComplete, onLoad, onRemoveItem, onReset, resetLabel, saveButtonLabel, secondarySaveButtonLabel, onReorder, showCellCount, onAddItem, className, renderSavePopup, renderWordSearch, hideControls, saveOnly, tableName, nameValue, onNameChange, descriptionValue, onDescriptionChange, namePlaceholder, descriptionPlaceholder, existingNames, editingName, onCancel, initialItemsPerPage, renderCellAction, actionsLeftExtra }) {
48
+ export default function WordTableBlock({ title, titleIcon, disc, filterTitle, filterCollapsible, filterDefaultCollapsed, filterCollapsed, onFilterCollapsedChange, filterHeaderSummary, brand, code, items, onSaveComplete, onLoad, onRemoveItem, onReset, resetLabel, saveButtonLabel, secondarySaveButtonLabel, onReorder, showCellCount, onAddItem, className, renderSavePopup, renderWordSearch, hideControls, saveOnly, tableName, nameValue, onNameChange, descriptionValue, onDescriptionChange, namePlaceholder, descriptionPlaceholder, existingNames, editingName, onCancel, initialItemsPerPage, renderCellAction, actionsLeftExtra }) {
49
49
  const { t } = useTranslation();
50
50
  const resolvedResetLabel = resetLabel ?? t('wordPrint.clearAllSelection', '모두 선택 해제');
51
51
  const isEditable = items === undefined;
@@ -76,6 +76,12 @@ export default function WordTableBlock({ title, titleIcon, disc, filterTitle, fi
76
76
  const [cellCount, setCellCount] = useState(4);
77
77
  const [activeCell, setActiveCell] = useState(null);
78
78
  const visibleCells = cells.slice(0, cellCount);
79
+ /* 띠를 켠 표면 설정 판에 '상단 정보'·'하단 정보' 고르개가 선다 — 스위치를 따로 두지
80
+ 않는다(부르는 쪽이 둘을 어긋나게 줄 일이 없게). 고른 값은 `brand.top`·`bottom` 을 덮는다. */
81
+ const 띠쓰나 = Boolean(brand?.show);
82
+ const [showTopInfo, setShowTopInfo] = useState(true);
83
+ const [showBottomInfo, setShowBottomInfo] = useState(true);
84
+ const 띠 = brand && { ...brand, top: showTopInfo, bottom: showBottomInfo };
79
85
  // 공통 단어 표 설정 상태
80
86
  const [orientation, setOrientation] = useState('landscape');
81
87
  const [contentTypes, setContentTypes] = useState(['이미지', '단어', '표준 발음']);
@@ -320,15 +326,15 @@ export default function WordTableBlock({ title, titleIcon, disc, filterTitle, fi
320
326
  setActiveCell(null);
321
327
  },
322
328
  onClose: handleCloseWordSearch,
323
- }), !hideControls && (_jsx(TableFilterPanel, { title: filterTitle, collapsible: filterCollapsible, defaultCollapsed: filterDefaultCollapsed, collapsed: filterCollapsed, onCollapsedChange: onFilterCollapsedChange, headerSummary: filterHeaderSummary, hasImage: hasImage, hasColloquial: hasColloquial, contentTypes: contentTypes, orientation: orientation, showOutline: showOutline, cutLine: cutLine, showDivLine: showDivLine, setters: { setContentTypes, setOrientation, setShowOutline, setCutLine, setShowDivLine }, showCutLine: (pagesEnabled ? itemsPerPage : count) > 1, showDivLine2: hasPairs, middleSections: middleSections, extraSelected: middleSelected, onExtraSelect: handleMiddleSelect })), isPaginated && pages ? (_jsx("div", { className: "word-table-block__pages", children: pages.map((page, i) => (_jsx(WordTable, { orientation: orientation, contentTypes: contentTypes, cutLine: cutLine, showOutline: showOutline, showDivLine: showDivLine, count: itemsPerPage, items: page, onRemoveItem: onRemoveItem,
329
+ }), !hideControls && (_jsx(TableFilterPanel, { title: filterTitle, collapsible: filterCollapsible, defaultCollapsed: filterDefaultCollapsed, collapsed: filterCollapsed, onCollapsedChange: onFilterCollapsedChange, headerSummary: filterHeaderSummary, hasImage: hasImage, hasColloquial: hasColloquial, contentTypes: contentTypes, orientation: orientation, showOutline: showOutline, cutLine: cutLine, showDivLine: showDivLine, setters: { setContentTypes, setOrientation, setShowOutline, setCutLine, setShowDivLine, setShowTopInfo, setShowBottomInfo }, showCutLine: (pagesEnabled ? itemsPerPage : count) > 1, showDivLine2: hasPairs, middleSections: middleSections, extraSelected: middleSelected, onExtraSelect: handleMiddleSelect, showPaperBands: 띠쓰나, showTopInfo: showTopInfo, showBottomInfo: showBottomInfo })), isPaginated && pages ? (_jsx("div", { className: "word-table-block__pages", children: pages.map((page, i) => (_jsx(WordTable, { orientation: orientation, contentTypes: contentTypes, cutLine: cutLine, showOutline: showOutline, showDivLine: showDivLine, count: itemsPerPage, items: page, onRemoveItem: onRemoveItem,
324
330
  /* 쪽이 나뉘면 끌어 옮기기가 통째로 꺼졌다(말귀 단어 찾기: 9개부터). 쪽 안의 자리를 전체 자리로 옮겨 넘긴다 —
325
331
  쪽을 넘는 이동은 하지 않는다. */
326
332
  onReorder: onReorder && ((from, to) => onReorder(from + i * itemsPerPage, to + i * itemsPerPage)),
327
333
  /* 안쪽 표는 제 쪽 안의 번호를 준다 — 부르는 쪽에는 **표 전체 기준**으로 넘긴다
328
334
  (위 `onReorder` 와 같은 셈이다. 쪽이 몇으로 나뉘었는지는 부르는 쪽 일이 아니다). */
329
- renderCellAction: renderCellAction && ((n, item) => renderCellAction(n + i * itemsPerPage, item)), contentRef: el => { pageRefs.current[i] = el; }, pageNumber: { current: i + 1, total: pages.length }, pageNumberPosition: pageNumberPosition }, i))) })) : (_jsx(WordTable, { orientation: orientation, contentTypes: contentTypes, cutLine: cutLine, showOutline: showOutline, showDivLine: showDivLine, count: count, items: isEditable ? undefined : (items ?? []), editCells: isEditable ? visibleCells : undefined, activeCell: activeCell, onActivateCell: (isEditable || onAddItem) ? setActiveCell : undefined, onRemoveCell: isEditable ? handleRemove : undefined, onRemoveItem: !isEditable ? onRemoveItem : undefined, onReorder: !isEditable ? onReorder : undefined,
335
+ renderCellAction: renderCellAction && ((n, item) => renderCellAction(n + i * itemsPerPage, item)), brand: 띠, code: code, contentRef: el => { pageRefs.current[i] = el; }, pageNumber: { current: i + 1, total: pages.length }, pageNumberPosition: pageNumberPosition }, i))) })) : (_jsx(WordTable, { orientation: orientation, contentTypes: contentTypes, cutLine: cutLine, showOutline: showOutline, showDivLine: showDivLine, count: count, items: isEditable ? undefined : (items ?? []), editCells: isEditable ? visibleCells : undefined, activeCell: activeCell, onActivateCell: (isEditable || onAddItem) ? setActiveCell : undefined, onRemoveCell: isEditable ? handleRemove : undefined, onRemoveItem: !isEditable ? onRemoveItem : undefined, onReorder: !isEditable ? onReorder : undefined,
330
336
  /* 편집 모드에는 얹을 항목이 없다 — 칸을 손으로 채우는 꼴이다 */
331
- renderCellAction: !isEditable ? renderCellAction : undefined, showFillerAdd: !!onAddItem, contentRef: contentRef })), inlineNameFields && !hideControls && (_jsxs(Panel, { size: "medium", labelWidth: 64, useBorder: false, useRowBorder: false, className: "word-table-block__meta", children: [_jsx(PanelRow, { variant: "custom", label: t('wordTable.save.nameLabel', '이름'), required: true, align: "vertical", useFullWidth: true, children: _jsx(TextField, { required: true, value: nameValue ?? '', onChange: onNameChange, placeholder: namePlaceholder ? namePlaceholder + t('wordTable.save.autoFillHint', ' (Tab 또는 space 입력시 자동 입력)') : t('wordTable.save.placeholder', '이 단어표에 대한 제목을 입력하세요.'),
337
+ renderCellAction: !isEditable ? renderCellAction : undefined, brand: 띠, code: code, showFillerAdd: !!onAddItem, contentRef: contentRef })), inlineNameFields && !hideControls && (_jsxs(Panel, { size: "medium", labelWidth: 64, useBorder: false, useRowBorder: false, className: "word-table-block__meta", children: [_jsx(PanelRow, { variant: "custom", label: t('wordTable.save.nameLabel', '이름'), required: true, align: "vertical", useFullWidth: true, children: _jsx(TextField, { required: true, value: nameValue ?? '', onChange: onNameChange, placeholder: namePlaceholder ? namePlaceholder + t('wordTable.save.autoFillHint', ' (Tab 또는 space 입력시 자동 입력)') : t('wordTable.save.placeholder', '이 단어표에 대한 제목을 입력하세요.'),
332
338
  /* PanelRow의 라벨은 시각적으로만 붙어 있고 입력과 연결돼 있지 않아, 낭독기에는
333
339
  이름 없는 입력으로 읽혔다. placeholder는 자동완성 안내문("Tab 또는 space…")이
334
340
  섞여 있어 이름으로 부적절하므로 같은 라벨 문구를 넘긴다. */
@@ -3,7 +3,7 @@ import { useMemo, useState, useCallback, useEffect } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { chunkItems } from '../wordTableLayout';
5
5
  import { getImageResolver, checkImageExists } from '../../utils/imageConfig';
6
- import { buildCompositionSection, buildOrientationSection, buildBorderSection, buildCutLineSection, buildDivLineSection, buildPageNumberSection, buildTopInfoSection, buildBottomInfoSection, buildItemsPerPageSection, buildTableSettingsSelected, applyTableSettingsSelect, PAGE_NUM_POSITION_VALUE, parsePageNumberPosition, itemsPerPageFromChoice, } from './wordTableSettings';
6
+ import { buildCompositionSection, buildOrientationSection, buildBorderSection, buildCutLineSection, buildDivLineSection, buildTopInfoSection, buildBottomInfoSection, buildItemsPerPageSection, buildTableSettingsSelected, applyTableSettingsSelect, PAGE_NUM_POSITION_VALUE, parsePageNumberPosition, itemsPerPageFromChoice, } from './wordTableSettings';
7
7
  /**
8
8
  * 표시 전용 단어표(편집 없음)의 '표 설정' FilterPanel 상태·섹션을 제공하는 훅.
9
9
  * 구성·용지 방향·외곽선, 내용이 있으면 페이지당 개수, 2페이지 이상이면 페이지 번호, 항목이 2개 이상이면
@@ -61,11 +61,13 @@ opts) {
61
61
  buildOrientationSection(t),
62
62
  // 내용이 있는(1개 이상) 표면 페이지당 개수를 항상 노출.
63
63
  ...(itemCount > 0 ? [buildItemsPerPageSection(t, { hasPairs, itemCount })] : []),
64
- /* '페이지 번호' 자리 고르개는 **띠를 켠 화면에서만** 걷는다(사용자 지시 2026-09-30) —
65
- 거기서는 쪽 번호가 바닥 띠 오른쪽에 고정으로 서므로 고를 자리가 없다. 띠를 안 쓰는
66
- 화면은 전과 똑같아야 한다: 한때 조건 없이 걷었는데, 그러면 이 기능을 쓰던 화면에서
67
- 절이 통째로 사라진다(말귀가 깔기 전에 짚었다, 2026-09-30). */
68
- ...(!띠쓰나 && isPaginated ? [buildPageNumberSection(t)] : []),
64
+ /* **'페이지 번호' 자리 고르개는 없앤다** — 띠를 쓰든 안 쓰든 아예 안 세운다.
65
+ 사용자 지시다(2026-09-30). `pageNumberPosition` 프롭은 남아 있어 부르는 쪽이 코드로
66
+ 자리를 정할 수는 있고, 다만 화면에서 고르게 하지 않는다.
67
+
68
+ **한 번 되돌렸다가 다시 걷었다.** 0.215.0 에서 '띠를 안 쓰는 화면의 회귀' 로 보고
69
+ 조건을 달아 되살렸는데, 그 판단의 바탕이 된 보고가 틀렸다(한 쪽짜리 표라 안 보였던
70
+ 것을 사라진 것으로 읽었다). 없애는 것이 본래 지시였다 — 다시 되살리지 말 것. */
69
71
  buildBorderSection(t),
70
72
  ...(count > 1 ? [buildCutLineSection(t)] : []),
71
73
  ...(hasPairs ? [buildDivLineSection(t)] : []),
@@ -3,6 +3,7 @@
3
3
  * WordTableBlock의 순수 타입 및 인터페이스 정의.
4
4
  */
5
5
  import type { ReactNode } from 'react';
6
+ import type { WordTableProps } from '../WordTable';
6
7
  import type { WordSearchResultItem } from './WordSearchResultBlock';
7
8
  export interface WordEntry {
8
9
  word: string;
@@ -49,6 +50,16 @@ export interface WordTableBlockProps {
49
50
  onFilterCollapsedChange?: (collapsed: boolean) => void;
50
51
  /** 그 판이 접혀도 머리 오른쪽에 남는 자리 — 요약. 안 주면 코어가 짓는다(`FilterPanel`). */
51
52
  filterHeaderSummary?: ReactNode;
53
+ /**
54
+ * 종이 머리·바닥 띠 — 안쪽 `WordTable` 로 그대로 간다.
55
+ *
56
+ * **`show` 가 켜지면 표 설정에 `상단 정보`·`하단 정보` 고르개가 저절로 선다** — 스위치를
57
+ * 둘로 나누면 부르는 쪽이 어긋나게 줄 수 있어서다(말귀 요청 ㉛, 2026-09-30). 그 고르개로
58
+ * 켜고 끈 값은 `top`·`bottom` 을 **덮어쓴다** — 화면에서 사람이 고른 것이 마지막 말이다.
59
+ */
60
+ brand?: WordTableProps['brand'];
61
+ /** 표 번호 — 띠 위 오른쪽에 선다. 안쪽 `WordTable` 로 그대로 간다. */
62
+ code?: string;
52
63
  disc?: ReactNode;
53
64
  /** 현재 표의 저장된 이름(불러온 표 등). 있으면 인쇄/PDF 저장 시 파일명으로 사용, 없으면 "오늘 날짜 + 단어표/대립쌍표" */
54
65
  tableName?: string;
@@ -65,23 +65,39 @@
65
65
 
66
66
  /* ── 종이 머리·바닥 띠 ────────────────────────────────────────────────
67
67
  쪽 번호가 쓰던 수를 위아래로 넓힌 것이다. 격자가 그만큼 비우고 띠는 그 빈 자리에
68
- 절대 배치로 선다 — **격자 안**이라 인쇄가 격자만 복사해 가도 함께 간다. */
69
- /* 띠 높이에 딱 붙인다 — 남는 틈은 4px 뿐이다(사용자 지시 2026-09-30: 여백이 많다).
70
- 비운 자리가 띠보다 크면 로고와 칸 사이가 벌어져 종이가 헐거워 보인다.
71
- **위아래를 따로 비운다** — 표 설정에서 한쪽만 끌 수 있다. */
72
- .word-table-block__content--brand-top { padding-top: calc(32px * var(--wt-unscale, 1)); }
73
- .word-table-block__content--brand-bottom { padding-bottom: calc(26px * var(--wt-unscale, 1)); }
74
- /* 절취선 바탕·외곽선은 칸이 있는 데까지만 — 띠까지 두르면 로고가 표 안에 든 것처럼 보인다
75
- (쪽 번호 때와 같은 까닭). 아래는 쪽 번호 규칙과 겹치므로 여기서 다시 적어 이긴다. */
68
+ 절대 배치로 선다 — **격자 안**이라 인쇄가 격자만 복사해 가도 함께 간다.
69
+
70
+ **치수는 변수로 연다**(말귀 요청 ㉜, 2026-09-30). 부르는 쪽이 `:root` 에 적으면
71
+ 화면과 **종이가 함께** 바뀐다 — 인쇄 문서는 스타일시트를 통째로 베껴 가므로 변수
72
+ 선언도 따라간다. 여기서는 기본값을 **선언하지 않고** `var(…, 기본)` 의 뒷값으로만
73
+ 둔다: 선언해 버리면 이 마디가 `:root` 보다 가까워 부르는 쪽 값이 진다.
74
+
75
+ | 변수 | 기본 |
76
+ |---|---|
77
+ | `--wt-band-top-height` · `--wt-band-top-size` · `--wt-band-top-align` | 28px · 11px · center |
78
+ | `--wt-band-bottom-height` · `--wt-band-bottom-size` | 22px · 8px |
79
+ | `--wt-brand-logo-height`(말귀) · `--wt-brand-ihab-height` · `--wt-brand-gap` | 28px · 17px · 8px |
80
+
81
+ **비우는 여백과 테두리 자리는 띠 높이에서 스스로 나온다.** 따로 박아 두었더니 띠만
82
+ 키웠을 때 띠가 표를 파고들고(위 −4px · 아래 −6px), 테두리가 칸 없는 빈 자리에 그려졌다.
83
+ 한 값에서 나오게 두면 그 둘이 다시 안 어긋난다. */
84
+ .word-table-block__content--brand-top {
85
+ padding-top: calc((var(--wt-band-top-height, 28px) + 4px) * var(--wt-unscale, 1));
86
+ }
87
+ .word-table-block__content--brand-bottom {
88
+ padding-bottom: calc((var(--wt-band-bottom-height, 22px) + 4px) * var(--wt-unscale, 1));
89
+ }
90
+ /* 절취선 바탕·외곽선은 칸이 있는 데까지만 — 띠까지 두르면 로고가 표 안에 든 것처럼 보인다 */
76
91
  .word-table-block__content--brand-top.word-table-block__content--cut-show,
77
92
  .word-table-block__content--brand-bottom.word-table-block__content--cut-show {
78
93
  background-clip: content-box;
79
94
  }
95
+ /* 위 여백과 **같은 셈**이다 — 따로 적지 않는다(그래서 어긋날 수가 없다) */
80
96
  .word-table-block__content--brand-top.word-table-block__content--outline::after {
81
- top: calc(32px * var(--wt-unscale, 1));
97
+ top: calc((var(--wt-band-top-height, 28px) + 4px) * var(--wt-unscale, 1));
82
98
  }
83
99
  .word-table-block__content--brand-bottom.word-table-block__content--outline::after {
84
- bottom: calc(26px * var(--wt-unscale, 1));
100
+ bottom: calc((var(--wt-band-bottom-height, 22px) + 4px) * var(--wt-unscale, 1));
85
101
  }
86
102
 
87
103
  .word-table__band {
@@ -89,63 +105,59 @@
89
105
  left: 0;
90
106
  right: 0;
91
107
  display: flex;
92
- align-items: center;
93
108
  justify-content: space-between;
94
109
  gap: calc(12px * var(--wt-unscale, 1));
95
110
  z-index: 2;
96
111
  pointer-events: none;
97
112
  color: var(--color-gray);
98
- /* 글꼴 크기는 띠마다 다르다 — 위는 caption-1, 아래는 caption-2(사용자 지시 2026-09-30).
99
- 클래스(`.caption-1`)를 그냥 붙이면 크기가 고정돼 배율 되돌림이 안 먹으므로 같은
100
- 토큰에 되돌림만 곱한다. 그 토큰은 보는 사람 쪽 설정(좁은 화면 · 큰 글씨)을 타므로
101
- 종이 글씨도 그만큼 달라진다 — 화면과 종이를 한 급으로 맞추기로 한 값이다. */
102
113
  font-weight: var(--text-caption-1-weight, 400);
103
114
  }
104
- /* 비워 둔 44px 안에 든다 */
105
- /* 위는 **11px**, 아래는 **8px** — 둘 다 종이 전용 값이라 토큰에서 벗어난다
106
- (사용자 지시 2026-09-30). 화면 글씨는 읽는 거리에 맞추지만 이 줄들은 종이에 찍혀
107
- 필요할 때만 들여다본다. 배율 되돌림은 그대로 받는다. */
115
+ /* 위는 11px, 아래는 8px 이 기본 — 둘 다 종이 전용 값이라 토큰에서 벗어난다
116
+ (사용자 지시 2026-09-30). 배율 되돌림은 그대로 받는다. */
108
117
  .word-table__band--top {
109
118
  top: 0;
110
- height: calc(28px * var(--wt-unscale, 1));
111
- font-size: calc(11px * var(--wt-unscale, 1));
112
- line-height: calc(15px * var(--wt-unscale, 1));
113
- }
114
- /* 아래는 **8px** 이다(사용자 지시 2026-09-30) — 만든 곳 · 주소 · 쪽 번호는 찾을 때만 읽는
115
- 잔글이라 디자인 시스템의 가장 작은 급(caption-2, 11px)보다 더 내린다. 토큰을 안 쓰는
116
- 유일한 자리이고, **종이 전용 값**이라 그렇다: 화면 글씨는 사람이 읽는 거리에 맞추지만
117
- 이 줄은 종이에 인쇄돼 필요할 때만 들여다본다. 배율 되돌림은 그대로 받는다. */
119
+ height: calc(var(--wt-band-top-height, 28px) * var(--wt-unscale, 1));
120
+ font-size: calc(var(--wt-band-top-size, 11px) * var(--wt-unscale, 1));
121
+ line-height: 1.35;
122
+ align-items: var(--wt-band-top-align, center);
123
+ }
118
124
  .word-table__band--bottom {
119
125
  /* 로고까지 이 색을 받는다 — 마스크를 `currentColor` 로 칠하므로 글씨와 늘 같은 색이다 */
120
126
  color: var(--color-gray);
121
127
  bottom: 0;
122
- height: calc(22px * var(--wt-unscale, 1));
123
- font-size: calc(8px * var(--wt-unscale, 1));
124
- line-height: calc(12px * var(--wt-unscale, 1));
128
+ height: calc(var(--wt-band-bottom-height, 22px) * var(--wt-unscale, 1));
129
+ font-size: calc(var(--wt-band-bottom-size, 8px) * var(--wt-unscale, 1));
130
+ line-height: 1.35;
131
+ align-items: var(--wt-band-bottom-align, center);
125
132
  }
126
133
 
127
134
  .word-table__brand-logo {
128
- height: calc(28px * var(--wt-unscale, 1));
135
+ height: calc(var(--wt-brand-logo-height, 28px) * var(--wt-unscale, 1));
129
136
  width: auto;
130
137
  display: block;
138
+ flex-shrink: 0;
131
139
  /* 말귀 로고는 컬러다 — 브라우저 인쇄 기본값은 바탕·색을 빼므로 그대로 두면 종이에서
132
140
  흐려지거나 사라진다(절취선 바탕이 쓰는 것과 같은 수다). */
133
141
  print-color-adjust: exact;
134
142
  -webkit-print-color-adjust: exact;
135
143
  }
144
+
136
145
  /* 만든 곳(ihab) 로고 — **그림이 아니라 꼴**이다. 흰 판 SVG 를 마스크로 오려 그 자리를
137
- `currentColor` 로 칠하므로 곁의 글씨와 **같은 색**이 된다(사용자 지시 2026-09-30).
138
- 파일에 `fill` 이 박혀 있어 `<img>` 로는 색을 못 바꾼다. 가로세로비는 파일 그대로(264:100).
139
- 인쇄에서도 색이 빠지지 않게 말귀 로고와 같은 수를 쓴다. */
146
+ `currentColor` 로 칠하므로 곁의 글씨와 **같은 색**이 된다(파일에 `fill` 이 박혀 있어
147
+ `<img>` 로는 색을 못 바꾼다).
148
+
149
+ **너비는 안 적는다.** `aspect-ratio` 로 파일 비(264:100)를 주고 높이 하나로 정하게 둔다 —
150
+ 너비를 `높이 × 2.64` 로 적어 두었더니, 높이만 올린 사람에게 **그림은 그대로였다**
151
+ (마스크가 `contain` 이라 좁은 쪽에 맞는다). 말귀 로고(`<img>`)와 같은 규약이 된다. */
140
152
  .word-table__brand-logo--ihab {
141
153
  display: block;
142
- height: calc(17px * var(--wt-unscale, 1));
143
- width: calc(17px * 2.64 * var(--wt-unscale, 1));
154
+ height: calc(var(--wt-brand-ihab-height, 17px) * var(--wt-unscale, 1));
155
+ width: auto;
156
+ aspect-ratio: 264 / 100;
144
157
  flex-shrink: 0;
145
158
  background-color: currentColor;
146
- /* 이 파일이 로고를 **직접** 가리킨다 — 묶개가 주소를 풀어 준다. 마디에 인라인으로
147
- 주던 때는 그 값이 화면에 안 실리는 일이 있었고, 무엇보다 여기 적어야 인쇄 문서로
148
- (스타일시트를 통째로 베껴 가므로) 함께 간다. */
159
+ /* 이 파일이 로고를 **직접** 가리킨다 — 묶개가 주소를 풀어 주고, 여기 적어야 인쇄
160
+ 문서로(스타일시트를 통째로 베껴 가므로) 함께 간다. */
149
161
  -webkit-mask-image: url('../logo/Logo_IHAB_en_white.svg');
150
162
  mask-image: url('../logo/Logo_IHAB_en_white.svg');
151
163
  -webkit-mask-repeat: no-repeat;
@@ -162,7 +174,7 @@
162
174
  .word-table__brand-by {
163
175
  display: flex;
164
176
  align-items: center;
165
- gap: calc(8px * var(--wt-unscale, 1));
177
+ gap: calc(var(--wt-brand-gap, 8px) * var(--wt-unscale, 1));
166
178
  min-width: 0;
167
179
  }
168
180
  .word-table__copyright {
@@ -171,7 +183,7 @@
171
183
  text-overflow: ellipsis;
172
184
  }
173
185
 
174
- /* 바닥 오른쪽 — 이 표의 주소와 쪽 번호. 주소가 길면(짧은 주소가 아닐 때) 먼저 양보한다. */
186
+ /* 바닥 오른쪽 — 이 표의 주소와 쪽 번호 */
175
187
  .word-table__band-end {
176
188
  display: flex;
177
189
  align-items: center;
@@ -179,10 +191,7 @@
179
191
  min-width: 0;
180
192
  }
181
193
 
182
- /* 표 번호·주소 — **종이 글씨는 전부 한 글꼴이다**(사용자 지시 2026-09-30). 고정폭으로
183
- 찍어 봤는데 한 장 안에 글꼴이 둘 섞여 보였다. 종이가 한 벌로 읽히는 쪽을 골랐다 —
184
- 목록 카드(`WordTableButton`)의 번호는 청한 대로 고정폭 그대로다. */
185
- /* 기관 이름 · 표 번호 — 번호만 진하게 둔다(찾는 눈이 번호에 먼저 닿게) */
194
+ /* 기관 이름 · 표 번호 */
186
195
  .word-table__ident {
187
196
  display: flex;
188
197
  align-items: center;
@@ -197,8 +206,8 @@
197
206
  white-space: nowrap;
198
207
  }
199
208
  .word-table__ident-dot { flex-shrink: 0; }
200
- /* 표 번호 — **띠와 같은 회색**이다(사용자 지시 2026-09-30). 한때 번호만 진하게 뒀는데,
201
- 머리 줄 전체가 종이를 식별하는 잔글이라 한 무게로 읽히는 편이 낫다. */
209
+ /* 표 번호 — **띠와 같은 회색**이다. 머리 줄 전체가 종이를 식별하는 잔글이라 한 무게로
210
+ 읽히는 편이 낫다. 종이 글씨는 전부 한 글꼴이다(고정폭은 목록 카드 쪽만). */
202
211
  .word-table__code {
203
212
  flex-shrink: 0;
204
213
  white-space: nowrap;
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.215.0";
49
+ export declare const CORE_VERSION = "0.217.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.215.0';
49
+ export const CORE_VERSION = '0.217.0';