@ihabdevteam/core 0.201.0 → 0.203.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.201.0";
1629
+ export declare const CORE_VERSION = "0.203.0";
1630
1630
 
1631
1631
  /** @public */
1632
1632
  declare const coreDefault: Record<string, unknown>;
@@ -10324,9 +10324,9 @@ export declare interface WordSearchResultWord {
10324
10324
  * 표시(items)·편집(editCells) 두 모드를 지원한다.
10325
10325
  * @public
10326
10326
  */
10327
- export declare function WordTable({ title, titleIcon, disc, className, orientation, contentTypes, cutLine, showOutline, showDivLine, items, editCells, count, activeCell, onActivateCell, onRemoveCell, onRemoveItem, onReorder, showFillerAdd, contentRef, buttonSize, pageNumber, pageNumberPosition, fitHeight, scoring, marks, onMarkClick, scoreIndexOffset, }: WordTableProps): JSX.Element;
10327
+ 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, pageNumber, pageNumberPosition, fitHeight, scoring, marks, onMarkClick, scoreIndexOffset, }: WordTableProps): JSX.Element;
10328
10328
 
10329
- export declare function WordTableBlock({ title, titleIcon, disc, 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 }: WordTableBlockProps): JSX.Element;
10329
+ export declare function WordTableBlock({ title, titleIcon, disc, 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 }: WordTableBlockProps): JSX.Element;
10330
10330
 
10331
10331
  /** @public */
10332
10332
  export declare interface WordTableBlockProps {
@@ -10393,6 +10393,34 @@ export declare interface WordTableBlockProps {
10393
10393
  hideControls?: boolean;
10394
10394
  /** 액션 중 저장 버튼만 노출 (불러오기/새 창/인쇄 숨김) */
10395
10395
  saveOnly?: boolean;
10396
+ /**
10397
+ * **'페이지당 개수' 로 들어설 때 골라져 있을 값** — 안 주면 지금까지처럼 8,
10398
+ * `'all'` 이면 한 쪽에 전부.
10399
+ *
10400
+ * 한 쪽에 다 담는 길은 이미 있었다(고르개 첫 칸이 늘 항목 수 그대로다). 막혀 있던 것은
10401
+ * **처음 값**뿐이라, 열두 낱말짜리 표를 만들어 놓고도 늘 여덟씩 두 쪽으로 시작해 사람이
10402
+ * 매번 `12개` 를 다시 눌러야 했다(말귀 간편 제작 — 요청 ㉑, 2026-09-27).
10403
+ *
10404
+ * **처음 값이지 매인 값이 아니다.** 들어설 때 한 번만 읽고, 그 뒤 사람이 고른 것을 덮지
10405
+ * 않는다 — 밖에서 계속 밀어 넣으면 사람이 고른 쪽을 앱이 도로 바꿔 버린다. 그래서 나중에
10406
+ * 이 값을 바꿔도 화면은 안 따라간다.
10407
+ *
10408
+ * **개수가 도중에 바뀌는 화면이면 `'all'` 을 준다.** 그러면 설정이 "전부" 로 남아 있어,
10409
+ * 낱말이 8개든 12개든 그때그때 한 쪽으로 선다 — 수를 바꿀 때마다 다시 넣어 줄 필요가 없다.
10410
+ * 숫자를 주면 그 숫자에 못 박히므로 도중에 늘어난 만큼은 다음 쪽으로 넘어간다.
10411
+ */
10412
+ initialItemsPerPage?: number | 'all';
10413
+ /**
10414
+ * **칸 오른쪽 위에 부르는 쪽이 얹는 것** — 안쪽 `WordTable` 의 같은 이름 프롭으로 그대로 간다.
10415
+ * 자리·드러나는 때·인쇄에서 치우는 규칙은 그쪽 글에 있다.
10416
+ *
10417
+ * **칸 번호는 표 전체 기준이다.** 쪽이 나뉘면 안쪽 표는 제 쪽 안의 번호(0부터)를 주는데,
10418
+ * 여기서 쪽 몫을 더해 넘긴다 — 부르는 쪽은 쪽이 몇으로 나뉘었는지 몰라도 된다
10419
+ * (`onReorder` 가 쓰는 셈과 같다).
10420
+ *
10421
+ * **표시 모드에만 간다** — 편집 모드(칸을 손으로 채우는 꼴)에는 얹을 항목이 없다.
10422
+ */
10423
+ renderCellAction?: (index: number, item: WordSearchResultItem) => ReactNode;
10396
10424
  }
10397
10425
 
10398
10426
  /**
@@ -10565,6 +10593,27 @@ export declare interface WordTableProps {
10565
10593
  onReorder?: (fromIndex: number, toIndex: number) => void;
10566
10594
  /** 표시 모드에서 `count` 안의 빈칸(항목 수 뒤 ~ count)에 '추가' 버튼 노출. count 가 항목 수와 같으면 그런 칸이 없다. `onActivateCell` 필요. */
10567
10595
  showFillerAdd?: boolean;
10596
+ /**
10597
+ * **칸 오른쪽 위에 부르는 쪽이 얹는 것** — 칸 번호와 그 칸의 항목을 준다. 안 주면 아무것도 안 얹는다.
10598
+ *
10599
+ * 표 전체를 다시 고르는 길(`onReshuffle` 을 쥔 화면)만 있으면 **여덟 칸 중 하나가 마음에 안 들어도
10600
+ * 나머지 일곱이 함께 바뀐다** — 치료사가 고른 것을 잃는다(말귀 요청 ⑳, 2026-09-27). 그 자리를 연다.
10601
+ *
10602
+ * **무엇을 할지가 아니라 어디에 놓을지만 코어가 안다.** 동작(`onReplaceItem` 같은 것)으로 받으면
10603
+ * 단추의 모양·이름·뜻까지 코어가 정하게 되는데, 이 자리는 화면마다 뜻이 다르다(한 곳은 '다른 쌍으로',
10604
+ * 다른 곳은 '이 낱말만 빼기' 일 수 있다). 그려 넣는 것은 부르는 쪽 몫이다.
10605
+ *
10606
+ * 코어가 쥐는 것 셋 —
10607
+ * · **자리**: 칸 오른쪽 위. 빼기(`onRemoveItem`)가 함께 서면 **한 줄로 나란히** 선다(지우는 것이
10608
+ * 오른쪽 끝 — `ItemActions` 와 같은 차례). 겹칠 일이 없다.
10609
+ * · **드러나는 때**: 칸에 마우스를 얹거나 초점이 들어올 때. 재생·정보·빼기와 같은 규칙이고,
10610
+ * hover 가 없는 기기에서는 늘 보인다.
10611
+ * · **인쇄·캡처에서는 안 나온다** — 만드는 동안만 쓰는 손잡이다. 화면 인쇄(`@media print`)와
10612
+ * 인쇄 창(`wordTablePrint`) 둘 다에서 치운다.
10613
+ *
10614
+ * **표시 모드(`items`)에만 있다** — 편집 모드(`cells`)에는 얹을 항목이 없다.
10615
+ */
10616
+ renderCellAction?: (index: number, item: WordSearchResultItem) => ReactNode;
10568
10617
  /** 그리드 콘텐츠 DOM ref — 인쇄/캡처용 */
10569
10618
  contentRef?: Ref<HTMLDivElement>;
10570
10619
  buttonSize?: number;
@@ -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, 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 }: WordTableBlockProps): import("react").JSX.Element;
4
+ export default function WordTableBlock({ title, titleIcon, disc, 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 }: 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, 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 }) {
48
+ export default function WordTableBlock({ title, titleIcon, disc, 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 }) {
49
49
  const { t } = useTranslation();
50
50
  const resolvedResetLabel = resetLabel ?? t('wordPrint.clearAllSelection', '모두 선택 해제');
51
51
  const isEditable = items === undefined;
@@ -86,8 +86,10 @@ export default function WordTableBlock({ title, titleIcon, disc, items, onSaveCo
86
86
  const contentRef = useRef(null);
87
87
  // 인쇄 캡처용 그리드 콘텐츠 ref 배열 (다중 페이지 — 페이지 분할 시)
88
88
  const pageRefs = useRef([]);
89
- // 페이지 분할(표시 모드 전용) — 페이지당 단어/대립쌍 개수. 기본값 8(=1페이지에 전부)
90
- const [itemsPerPageSetting, setItemsPerPageSetting] = useState(8);
89
+ /* 페이지 분할(표시 모드 전용) — 페이지당 단어/대립쌍 개수.
90
+ **들어설 때 한 번만 읽는다.** 그 뒤 사람이 고른 것을 밖에서 덮지 않는다(`initialItemsPerPage`).
91
+ `'all'` 은 무한대로 둔다 — 아래에서 항목 수로 조이므로 늘 한 쪽이 된다. */
92
+ const [itemsPerPageSetting, setItemsPerPageSetting] = useState(initialItemsPerPage === 'all' ? Number.POSITIVE_INFINITY : initialItemsPerPage ?? 8);
91
93
  // 종이 안에 페이지 번호(1 / N)를 찍을 위치 — 미표시 포함
92
94
  const [pageNumberPosition, setPageNumberPosition] = useState('right');
93
95
  // 내부 저장 팝업
@@ -319,7 +321,12 @@ export default function WordTableBlock({ title, titleIcon, disc, items, onSaveCo
319
321
  }), !hideControls && (_jsx(TableFilterPanel, { 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,
320
322
  /* 쪽이 나뉘면 끌어 옮기기가 통째로 꺼졌다(말귀 단어 찾기: 9개부터). 쪽 안의 자리를 전체 자리로 옮겨 넘긴다 —
321
323
  쪽을 넘는 이동은 하지 않는다. */
322
- onReorder: onReorder && ((from, to) => onReorder(from + i * itemsPerPage, to + i * itemsPerPage)), 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, 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', '이 단어표에 대한 제목을 입력하세요.'),
324
+ onReorder: onReorder && ((from, to) => onReorder(from + i * itemsPerPage, to + i * itemsPerPage)),
325
+ /* 안쪽 표는 제 쪽 안의 번호를 준다 — 부르는 쪽에는 **표 전체 기준**으로 넘긴다
326
+ (위 `onReorder` 와 같은 셈이다. 쪽이 몇으로 나뉘었는지는 부르는 쪽 일이 아니다). */
327
+ 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,
328
+ /* 편집 모드에는 얹을 항목이 없다 — 칸을 손으로 채우는 꼴이다 */
329
+ 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', '이 단어표에 대한 제목을 입력하세요.'),
323
330
  /* PanelRow의 라벨은 시각적으로만 붙어 있고 입력과 연결돼 있지 않아, 낭독기에는
324
331
  이름 없는 입력으로 읽혔다. placeholder는 자동완성 안내문("Tab 또는 space…")이
325
332
  섞여 있어 이름으로 부적절하므로 같은 라벨 문구를 넘긴다. */
@@ -79,6 +79,34 @@ export interface WordTableBlockProps {
79
79
  hideControls?: boolean;
80
80
  /** 액션 중 저장 버튼만 노출 (불러오기/새 창/인쇄 숨김) */
81
81
  saveOnly?: boolean;
82
+ /**
83
+ * **'페이지당 개수' 로 들어설 때 골라져 있을 값** — 안 주면 지금까지처럼 8,
84
+ * `'all'` 이면 한 쪽에 전부.
85
+ *
86
+ * 한 쪽에 다 담는 길은 이미 있었다(고르개 첫 칸이 늘 항목 수 그대로다). 막혀 있던 것은
87
+ * **처음 값**뿐이라, 열두 낱말짜리 표를 만들어 놓고도 늘 여덟씩 두 쪽으로 시작해 사람이
88
+ * 매번 `12개` 를 다시 눌러야 했다(말귀 간편 제작 — 요청 ㉑, 2026-09-27).
89
+ *
90
+ * **처음 값이지 매인 값이 아니다.** 들어설 때 한 번만 읽고, 그 뒤 사람이 고른 것을 덮지
91
+ * 않는다 — 밖에서 계속 밀어 넣으면 사람이 고른 쪽을 앱이 도로 바꿔 버린다. 그래서 나중에
92
+ * 이 값을 바꿔도 화면은 안 따라간다.
93
+ *
94
+ * **개수가 도중에 바뀌는 화면이면 `'all'` 을 준다.** 그러면 설정이 "전부" 로 남아 있어,
95
+ * 낱말이 8개든 12개든 그때그때 한 쪽으로 선다 — 수를 바꿀 때마다 다시 넣어 줄 필요가 없다.
96
+ * 숫자를 주면 그 숫자에 못 박히므로 도중에 늘어난 만큼은 다음 쪽으로 넘어간다.
97
+ */
98
+ initialItemsPerPage?: number | 'all';
99
+ /**
100
+ * **칸 오른쪽 위에 부르는 쪽이 얹는 것** — 안쪽 `WordTable` 의 같은 이름 프롭으로 그대로 간다.
101
+ * 자리·드러나는 때·인쇄에서 치우는 규칙은 그쪽 글에 있다.
102
+ *
103
+ * **칸 번호는 표 전체 기준이다.** 쪽이 나뉘면 안쪽 표는 제 쪽 안의 번호(0부터)를 주는데,
104
+ * 여기서 쪽 몫을 더해 넘긴다 — 부르는 쪽은 쪽이 몇으로 나뉘었는지 몰라도 된다
105
+ * (`onReorder` 가 쓰는 셈과 같다).
106
+ *
107
+ * **표시 모드에만 간다** — 편집 모드(칸을 손으로 채우는 꼴)에는 얹을 항목이 없다.
108
+ */
109
+ renderCellAction?: (index: number, item: WordSearchResultItem) => ReactNode;
82
110
  }
83
111
  export declare const CELL_COUNTS: number[];
84
112
  /** 표시(인쇄) 모드에서 선택 가능한 "페이지당 개수"(단어/대립쌍 수) — 이 값 그대로 페이지당 칸 수가 된다 */
@@ -141,7 +141,10 @@ export function captureHtml(els, paperW, paperH, autoPrint = false, fileName = '
141
141
  flex-shrink: 0;
142
142
  transform-origin: top center;
143
143
  }
144
- .word-table-block__card-close { display: none; }
144
+ /* 만드는 동안만 쓰는 손잡이들 — 종이에는 안 나간다(renderCellAction 이 얹은 것도 이 줄 안에 있다).
145
+ **여기는 템플릿 글 안이다** — 홑따옴표 대신 역따옴표를 쓰면 글이 거기서 끊긴다. */
146
+ .word-table-block__card-close,
147
+ .word-table-block__card-actions { display: none; }
145
148
  /* 머리글은 격자 위에 얹힌다. 용지 안쪽 여백(57px)을 이미 .paper-wrap 이 갖고 있어
146
149
  여기서는 아래 간격만 준다. 격자는 제 높이를 스스로 잡으므로 밀려도 잘리지 않는다. */
147
150
  .paper-head {
@@ -25,10 +25,39 @@
25
25
  box-sizing: border-box;
26
26
  }
27
27
 
28
- /* ── 페이지 번호(종이 안에 직접 표기) ──────────────────── */
28
+ /* ── 페이지 번호 ───────────────────────────────────────── */
29
+
30
+ /* **격자가 번호 몫을 비워 준다.** `bottom` 이 양수(8px)인데 비워 둔 자리가 없어서, 번호가
31
+ **마지막 칸 위에 겹쳐** 있었다(실측 2026-09-27: 번호 y 570~592, 마지막 칸 y 262~601 —
32
+ 사장님이 "칸 안으로 들어와 있네" 로 짚으신 것이다). 격자가 종이 안쪽을 꽉 채우므로
33
+ (`height: 100%`) 비워 주지 않으면 번호가 설 자리가 없다.
34
+
35
+ **격자 밖으로 낼 수는 없다** — 인쇄가 베끼는 것이 이 격자다(`contentRef` 가 가리키는
36
+ 요소를 통째로 복사해 인쇄 문서에 넣는다). 밖으로 내면 종이에서 번호가 사라진다.
37
+ 음수 `bottom` 으로 종이 여백에 앉히는 길도 접었다 — 이 저장소는 **밖으로 내민 절대배치**를
38
+ 전수로 막고 있고(`tableFieldResizer` 시험), 그 봐줄 목록은 "부모가 잘라 준다" 를 근거로
39
+ 요구한다. 여기는 잘리면 안 되는 자리라 그 근거를 댈 수 없다.
40
+
41
+ 그래서 **번호가 있을 때만** 격자가 아래를 그만큼 비운다. 칸은 그만큼 낮아지는데, 그게
42
+ 번호가 설 자리다. 번호가 없는 표는 한 픽셀도 안 바뀐다. */
43
+ .word-table-block__content--page-number {
44
+ padding-bottom: 28px;
45
+ }
46
+ /* 절취선 바탕은 **칸이 있는 데까지만** 칠한다 — 안 그러면 비워 둔 띠가 선 색으로 꽉 찬다
47
+ (`--cut-show` 는 격자 바탕을 선 색으로 깔고 칸이 그 위를 덮는 꼴이다). 칸 사이 틈은
48
+ content-box 안이라 그대로 남는다. */
49
+ .word-table-block__content--page-number.word-table-block__content--cut-show {
50
+ background-clip: content-box;
51
+ }
52
+ /* 외곽선도 칸이 있는 데까지 — 비워 둔 띠까지 두르면 번호가 표 안에 든 것처럼 보인다 */
53
+ .word-table-block__content--page-number.word-table-block__content--outline::after {
54
+ bottom: 28px;
55
+ }
56
+
29
57
  .word-table__page-number {
30
58
  position: absolute;
31
- bottom: 8px;
59
+ /* 비워 둔 28px 안이다 — 밖으로 내밀지 않는다 */
60
+ bottom: 4px;
32
61
  z-index: 2;
33
62
  color: var(--color-gray);
34
63
  pointer-events: none;
@@ -149,7 +178,34 @@
149
178
  transition: opacity 0.15s;
150
179
  }
151
180
 
181
+ /* **오른쪽 위 한 줄** — 부르는 쪽이 얹은 것(`renderCellAction`)과 빼기가 함께 설 때 쓴다.
182
+ 둘 다 같은 모서리를 원해서, 각자 `position: absolute` 로 두면 그대로 겹친다. 줄이 자리를
183
+ 한 번만 잡고 안엣것들은 흐름으로 선다. 빼기는 그 안에서 **오른쪽 끝**이다 — 지우는 것이
184
+ 끝에 서는 차례는 `ItemActions` 와 같다. */
185
+ .word-table-block__card-actions {
186
+ position: absolute;
187
+ top: 20px;
188
+ right: 20px;
189
+ z-index: 1;
190
+ display: flex;
191
+ align-items: center;
192
+ gap: var(--spacing-xs);
193
+ opacity: 0;
194
+ transition: opacity 0.15s;
195
+ }
196
+ /* 줄 안에 들면 제 자리를 안 잡는다 — 줄이 잡아 준다. 편집 모드(`cells`)의 빼기는 줄 없이
197
+ 혼자 서므로 위 규칙이 그대로 산다. */
198
+ .word-table-block__card-actions .word-table-block__card-close {
199
+ position: static;
200
+ top: auto;
201
+ right: auto;
202
+ opacity: 1;
203
+ }
204
+
152
205
  /* 키보드 대응 — opacity:0인 채로 포커스가 가면 사용자는 보이지 않는 버튼을 누르게 된다 */
206
+ .word-table-block__card:focus-within .word-table-block__card-actions,
207
+ .word-table-block__card-actions:focus-within,
208
+ .word-table-block__card:hover .word-table-block__card-actions,
153
209
  .word-table-block__card:focus-within .word-table-block__card-close,
154
210
  /* 이 요소들은 **버튼을 감싼 div**라 자신은 포커스를 못 받는다 — :focus-visible이 아니라
155
211
  :focus-within으로 봐야 안의 버튼이 포커스를 받았을 때 드러난다(실측으로 확인). */
@@ -309,6 +365,7 @@
309
365
  (보이지 않는 단추를 짐작으로 눌러야 했다). 늘 보이게만 한다 — 색·모양은 그대로. 인쇄에서는 아래 @media print 가 치운다. */
310
366
  @media (hover: none) {
311
367
  .word-table-block__card-close,
368
+ .word-table-block__card-actions,
312
369
  .word-table-block__card-handle,
313
370
  .word-table-cell__play-button,
314
371
  .word-table-cell__info-button {
@@ -400,6 +457,7 @@
400
457
  }
401
458
 
402
459
  .word-table-block__card-close,
460
+ .word-table-block__card-actions,
403
461
  .word-table-block__card-handle { display: none; }
404
462
 
405
463
  .word-table-cell__play-button,
@@ -38,6 +38,27 @@ export interface WordTableProps {
38
38
  onReorder?: (fromIndex: number, toIndex: number) => void;
39
39
  /** 표시 모드에서 `count` 안의 빈칸(항목 수 뒤 ~ count)에 '추가' 버튼 노출. count 가 항목 수와 같으면 그런 칸이 없다. `onActivateCell` 필요. */
40
40
  showFillerAdd?: boolean;
41
+ /**
42
+ * **칸 오른쪽 위에 부르는 쪽이 얹는 것** — 칸 번호와 그 칸의 항목을 준다. 안 주면 아무것도 안 얹는다.
43
+ *
44
+ * 표 전체를 다시 고르는 길(`onReshuffle` 을 쥔 화면)만 있으면 **여덟 칸 중 하나가 마음에 안 들어도
45
+ * 나머지 일곱이 함께 바뀐다** — 치료사가 고른 것을 잃는다(말귀 요청 ⑳, 2026-09-27). 그 자리를 연다.
46
+ *
47
+ * **무엇을 할지가 아니라 어디에 놓을지만 코어가 안다.** 동작(`onReplaceItem` 같은 것)으로 받으면
48
+ * 단추의 모양·이름·뜻까지 코어가 정하게 되는데, 이 자리는 화면마다 뜻이 다르다(한 곳은 '다른 쌍으로',
49
+ * 다른 곳은 '이 낱말만 빼기' 일 수 있다). 그려 넣는 것은 부르는 쪽 몫이다.
50
+ *
51
+ * 코어가 쥐는 것 셋 —
52
+ * · **자리**: 칸 오른쪽 위. 빼기(`onRemoveItem`)가 함께 서면 **한 줄로 나란히** 선다(지우는 것이
53
+ * 오른쪽 끝 — `ItemActions` 와 같은 차례). 겹칠 일이 없다.
54
+ * · **드러나는 때**: 칸에 마우스를 얹거나 초점이 들어올 때. 재생·정보·빼기와 같은 규칙이고,
55
+ * hover 가 없는 기기에서는 늘 보인다.
56
+ * · **인쇄·캡처에서는 안 나온다** — 만드는 동안만 쓰는 손잡이다. 화면 인쇄(`@media print`)와
57
+ * 인쇄 창(`wordTablePrint`) 둘 다에서 치운다.
58
+ *
59
+ * **표시 모드(`items`)에만 있다** — 편집 모드(`cells`)에는 얹을 항목이 없다.
60
+ */
61
+ renderCellAction?: (index: number, item: WordSearchResultItem) => ReactNode;
41
62
  /** 그리드 콘텐츠 DOM ref — 인쇄/캡처용 */
42
63
  contentRef?: Ref<HTMLDivElement>;
43
64
  buttonSize?: number;
@@ -93,4 +114,4 @@ export interface WordTableProps {
93
114
  * 표시(items)·편집(editCells) 두 모드를 지원한다.
94
115
  * @public
95
116
  */
96
- export default function WordTable({ title, titleIcon, disc, className, orientation, contentTypes, cutLine, showOutline, showDivLine, items, editCells, count, activeCell, onActivateCell, onRemoveCell, onRemoveItem, onReorder, showFillerAdd, contentRef, buttonSize, pageNumber, pageNumberPosition, fitHeight, scoring, marks, onMarkClick, scoreIndexOffset, }: WordTableProps): import("react").JSX.Element;
117
+ export default function WordTable({ title, titleIcon, disc, className, orientation, contentTypes, cutLine, showOutline, showDivLine, items, editCells, count, activeCell, onActivateCell, onRemoveCell, onRemoveItem, onReorder, renderCellAction, showFillerAdd, contentRef, buttonSize, pageNumber, pageNumberPosition, fitHeight, scoring, marks, onMarkClick, scoreIndexOffset, }: WordTableProps): import("react").JSX.Element;
@@ -184,7 +184,7 @@ function 어종이름(t) {
184
184
  * 표시(items)·편집(editCells) 두 모드를 지원한다.
185
185
  * @public
186
186
  */
187
- export default function WordTable({ title, titleIcon, disc, className, orientation = 'landscape', contentTypes = ['이미지', '단어', '표준 발음'], cutLine = '표시', showOutline = true, showDivLine = true, items, editCells, count, activeCell = null, onActivateCell, onRemoveCell, onRemoveItem, onReorder, showFillerAdd = false, contentRef, buttonSize = 40, pageNumber, pageNumberPosition = 'right', fitHeight = false, scoring = false, marks, onMarkClick, scoreIndexOffset = 0, }) {
187
+ export default function WordTable({ title, titleIcon, disc, className, orientation = 'landscape', contentTypes = ['이미지', '단어', '표준 발음'], cutLine = '표시', showOutline = true, showDivLine = true, items, editCells, count, activeCell = null, onActivateCell, onRemoveCell, onRemoveItem, onReorder, renderCellAction, showFillerAdd = false, contentRef, buttonSize = 40, pageNumber, pageNumberPosition = 'right', fitHeight = false, scoring = false, marks, onMarkClick, scoreIndexOffset = 0, }) {
188
188
  const clickMark = useCallback((cellIndex, clicked) => {
189
189
  onMarkClick?.(scoreIndexOffset + cellIndex, clicked);
190
190
  }, [onMarkClick, scoreIndexOffset]);
@@ -363,7 +363,9 @@ export default function WordTable({ title, titleIcon, disc, className, orientati
363
363
  const hasPairs = isEdit
364
364
  ? (editCells ?? []).some(c => c !== null && c.length > 1)
365
365
  : (items ?? []).some(item => (item.words ?? []).length > 1);
366
- const contentClass = cx('word-table-block__content', CUT_CLASS[cutLine], showOutline && 'word-table-block__content--outline', showDivLine && hasPairs && 'word-table-block__content--div-line');
366
+ const contentClass = cx('word-table-block__content', CUT_CLASS[cutLine], showOutline && 'word-table-block__content--outline', showDivLine && hasPairs && 'word-table-block__content--div-line',
367
+ /* 번호가 설 자리를 비운다 — 번호가 없으면 비우지 않는다(표가 공연히 낮아지지 않게) */
368
+ pageNumber && pageNumberPosition !== 'none' && 'word-table-block__content--page-number');
367
369
  // 빈칸(추가버튼)은 스케일 역보정으로 실제 크기 유지
368
370
  const counterScale = { transform: `scale(${1 / scale})`, transformOrigin: 'center center' };
369
371
  /* **채점 단추는 표와 함께 줄어들면 안 된다.**
@@ -390,7 +392,9 @@ export default function WordTable({ title, titleIcon, disc, className, orientati
390
392
  ariaLabel: t('wordTable.reorder.handle', {
391
393
  word: (item.words ?? []).map(w => w.label).join(' / '),
392
394
  defaultValue: '{{word}} 순서 바꾸기 (Alt + 화살표)',
393
- }) }) })), onRemoveItem && (_jsx("div", { className: "word-table-block__card-close", children: _jsx(Button, { htmlType: "button", size: buttonSize, type: "dot", icon: "x", variant: "transparent-lightGray", useBorder: false, showTooltipInfo: (item.words ?? []).length > 1 ? t('wordPrint.removePair', '대립쌍 빼기') : t('wordPrint.removeWord', '단어 빼기'), ariaLabel: (item.words ?? []).length > 1 ? t('wordPrint.removePair', '대립쌍 빼기') : t('wordPrint.removeWord', '단어 빼기'), onClick: () => onRemoveItem(item.id) }) }))] }, item.id))), Array.from({ length: Math.max(0, cols * rows - (items?.length ?? 0)) }).map((_, i) => {
395
+ }) }) })), (renderCellAction || onRemoveItem) && (
396
+ /* 오른쪽 위 한 줄 — 부르는 쪽이 얹은 것과 빼기가 나란히 선다(지우는 것이 끝) */
397
+ _jsxs("div", { className: "word-table-block__card-actions", children: [renderCellAction?.(index, item), onRemoveItem && (_jsx("div", { className: "word-table-block__card-close", children: _jsx(Button, { htmlType: "button", size: buttonSize, type: "dot", icon: "x", variant: "transparent-lightGray", useBorder: false, showTooltipInfo: (item.words ?? []).length > 1 ? t('wordPrint.removePair', '대립쌍 빼기') : t('wordPrint.removeWord', '단어 빼기'), ariaLabel: (item.words ?? []).length > 1 ? t('wordPrint.removePair', '대립쌍 빼기') : t('wordPrint.removeWord', '단어 빼기'), onClick: () => onRemoveItem(item.id) }) }))] }))] }, item.id))), Array.from({ length: Math.max(0, cols * rows - (items?.length ?? 0)) }).map((_, i) => {
394
398
  const isWithinCount = (items?.length ?? 0) + i < cellCount;
395
399
  return (_jsx("div", { className: "word-table-block__card word-table-block__card--filler", children: showFillerAdd && onActivateCell && isWithinCount && (_jsx("div", { style: counterScale, children: _jsx(Button, { htmlType: "button", size: buttonSize, type: "bar", icon: "plus", label: t('wordTable.addWord', '추가하기'), onClick: () => onActivateCell((items?.length ?? 0) + i) }) })) }, `filler-${i}`));
396
400
  })] })), pageNumber && pageNumberPosition !== 'none' && (_jsx("span", { className: `word-table__page-number body-3 word-table__page-number--${pageNumberPosition}`, children: t('wordTable.filter.pageIndicator', { current: pageNumber.current, total: pageNumber.total }) }))] }) }), onReorder && _jsx("span", { role: "status", style: 보이지않게, children: 옮김알림 })] }));
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.201.0";
49
+ export declare const CORE_VERSION = "0.203.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.201.0';
49
+ export const CORE_VERSION = '0.203.0';