@ihabdevteam/core 0.321.0 → 0.322.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -0
- package/dist/components.cjs.js +3 -3
- package/dist/hooks.cjs.js +3 -3
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +5 -5
- package/dist/ihabdevteam-core.d.ts +64 -60
- package/dist/packages/core/src/components/HearingDeviceInfo.d.ts +5 -5
- package/dist/packages/core/src/components/HearingDeviceInfo.js +2 -2
- package/dist/packages/core/src/components/PanelRow.d.ts +2 -4
- package/dist/packages/core/src/components/PanelRow.js +2 -2
- package/dist/packages/core/src/components/Popup/DeviceListPopup.d.ts +4 -8
- package/dist/packages/core/src/components/Popup/DeviceListPopup.js +1 -1
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +3 -3
- package/dist/packages/core/src/components/Popup/ListPopup.js +3 -3
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +3 -3
- package/dist/packages/core/src/components/Training/NoisePlayerContext.d.ts +7 -4
- package/dist/packages/core/src/components/Training/TrainingBlock.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +2 -2
- package/dist/packages/core/src/components/Training/oneShotSound.d.ts +1 -1
- package/dist/packages/core/src/components/Training/oneShotSound.js +3 -3
- package/dist/packages/core/src/components/Training/stepAnswerable.d.ts +2 -2
- package/dist/packages/core/src/components/Training/trainingMissStats.js +2 -2
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +9 -9
- package/dist/packages/core/src/components/Word/wordScoreStats.js +2 -2
- package/dist/packages/core/src/components/Word/wordTableRecommend.js +2 -2
- package/dist/packages/core/src/components/wordTableLayout.d.ts +5 -3
- package/dist/packages/core/src/hooks/inputModality.d.ts +1 -1
- package/dist/packages/core/src/hooks/inputModality.js +1 -1
- package/dist/packages/core/src/hooks/useDeviceManagement.d.ts +5 -17
- package/dist/packages/core/src/hooks/useDeviceManagement.js +20 -8
- package/dist/packages/core/src/hooks/usePageResetOnChange.d.ts +3 -3
- package/dist/packages/core/src/hooks/usePageResetOnChange.js +6 -6
- package/dist/packages/core/src/hooks/useTablePaging.d.ts +16 -5
- package/dist/packages/core/src/hooks/useTablePaging.js +20 -9
- package/dist/packages/core/src/hooks/useWordTrainingFilter.d.ts +3 -1
- package/dist/packages/core/src/hooks/useWordTrainingFilter.js +7 -5
- package/dist/packages/core/src/utils/devWarn.d.ts +2 -2
- package/dist/packages/core/src/utils/devWarn.js +5 -5
- package/dist/packages/core/src/utils/filterPanelSummary.d.ts +1 -1
- package/dist/packages/core/src/utils/filterPanelSummary.js +4 -4
- package/dist/packages/core/src/utils/navScroll.d.ts +2 -2
- package/dist/packages/core/src/utils/navScroll.js +4 -4
- package/dist/packages/core/src/utils/pressable.d.ts +1 -1
- package/dist/packages/core/src/utils/pressable.js +3 -3
- package/dist/packages/core/src/utils/sessionTab.d.ts +7 -7
- package/dist/packages/core/src/utils/sessionTab.js +9 -9
- package/dist/packages/core/src/utils/textSort.d.ts +15 -5
- package/dist/packages/core/src/utils/textSort.js +19 -9
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +2 -2
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +3 -3
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +1 -1
- package/package.json +1 -1
|
@@ -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 {
|
|
4
|
+
import { compareKorean } from '../../utils/textSort';
|
|
5
5
|
import { useEscToClose } from '../../hooks/useEscToClose';
|
|
6
6
|
import { useState, useCallback, useMemo, useEffect } from 'react';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
|
@@ -86,7 +86,7 @@ export default function WordSearchResultBlock({ titleIcon, resultCount, resultUn
|
|
|
86
86
|
};
|
|
87
87
|
}, [q]);
|
|
88
88
|
/* 정렬 비교자 — flat/그룹 뷰가 공유한다.
|
|
89
|
-
**글자 견주기는
|
|
89
|
+
**글자 견주기는 `compareKorean`(콜레이터 하나) 으로만 한다**(말귀 요청 (113), 2026-10-10). 전에는 '시작 우선'
|
|
90
90
|
갈래만 콜레이터를 쓰고 **여섯 갈래는 `localeCompare(…, 'ko')`** 였다 — 그 부름은 비교마다 콜레이터를
|
|
91
91
|
새로 만든다. 하필 기본 정렬이 `ganada` 라 **주된 길이 느린 쪽**이었다(말귀 실측 6,357개: 30.3ms → 8.1ms).
|
|
92
92
|
이 파일에 콜레이터가 하나 더 있었던 것(`new Intl.Collator('ko')`)도 걷었다 — 같은 결정이 두 자리에
|
|
@@ -104,14 +104,14 @@ export default function WordSearchResultBlock({ titleIcon, resultCount, resultUn
|
|
|
104
104
|
// 시작 우선 — 검색어로 시작하는 단어를 위로. 앞 q 글자만 잘라 매칭하면 "첫 음절부터 일치"만
|
|
105
105
|
// 남는다(초성 검색도 동일: 'ㄴ' → '나무'가 위). 동순위는 가나다.
|
|
106
106
|
case 'match': {
|
|
107
|
-
return 시작순위(a) - 시작순위(b) ||
|
|
107
|
+
return 시작순위(a) - 시작순위(b) || compareKorean(label(a), label(b));
|
|
108
108
|
}
|
|
109
|
-
case 'ganada': return
|
|
110
|
-
case 'ganada_desc': return
|
|
111
|
-
case 'syllable': return syll(a) - syll(b) ||
|
|
112
|
-
case 'syllable_desc': return syll(b) - syll(a) ||
|
|
113
|
-
case 'grade': return grade(a) - grade(b) ||
|
|
114
|
-
case 'grade_desc': return grade(b) - grade(a) ||
|
|
109
|
+
case 'ganada': return compareKorean(label(a), label(b));
|
|
110
|
+
case 'ganada_desc': return compareKorean(label(b), label(a));
|
|
111
|
+
case 'syllable': return syll(a) - syll(b) || compareKorean(label(a), label(b));
|
|
112
|
+
case 'syllable_desc': return syll(b) - syll(a) || compareKorean(label(a), label(b));
|
|
113
|
+
case 'grade': return grade(a) - grade(b) || compareKorean(label(a), label(b));
|
|
114
|
+
case 'grade_desc': return grade(b) - grade(a) || compareKorean(label(a), label(b));
|
|
115
115
|
// 받은 순서 — Array.prototype.sort 는 안정 정렬이라 0 이면 받은 차례가 남는다
|
|
116
116
|
default: return 0;
|
|
117
117
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { compareKorean } from '../../utils/textSort';
|
|
2
2
|
import { labelOfCellKey } from './wordTableReview';
|
|
3
3
|
/** △ 를 반 점으로 치는 셈 — 저장할 때 서버가 쓰는 규칙과 같다. */
|
|
4
4
|
function 점수(o, triangle, marked) {
|
|
@@ -46,7 +46,7 @@ export function weakItems(records) {
|
|
|
46
46
|
const tried = v.o + v.triangle + v.x;
|
|
47
47
|
out.push({ label: labelOfCellKey(key), key, tried, o: v.o, triangle: v.triangle, x: v.x, rate: 점수(v.o, v.triangle, tried), latest: v.latest, latestAt: v.latestAt });
|
|
48
48
|
}
|
|
49
|
-
out.sort((a, b) => a.rate - b.rate || b.tried - a.tried ||
|
|
49
|
+
out.sort((a, b) => a.rate - b.rate || b.tried - a.tried || compareKorean(a.label, b.label) || compareKorean(a.key ?? a.label, b.key ?? b.label));
|
|
50
50
|
return out;
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 화면도 통신도 없다. 표 목록과 약한 낱말만 받아 순서를 매긴다.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { compareKorean } from '../../utils/textSort';
|
|
11
11
|
/**
|
|
12
12
|
* 약한 낱말을 많이 품은 표부터 내놓는다.
|
|
13
13
|
*
|
|
@@ -44,7 +44,7 @@ export function recommendTables(tables, weakLabels, limit = 5) {
|
|
|
44
44
|
}
|
|
45
45
|
out.sort((a, b) => b.covered.length - a.covered.length
|
|
46
46
|
|| a.totalCells - b.totalCells
|
|
47
|
-
||
|
|
47
|
+
|| compareKorean(a.name, b.name));
|
|
48
48
|
return out.slice(0, Math.max(0, limit));
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
@@ -67,7 +67,9 @@ export declare const POS_LABEL: Record<string, string>;
|
|
|
67
67
|
/** 어종(단어 유형) 약호 → 전체 표기. 데이터의 Class 컬럼은 축약형('고유'/'한자'/'외래'/'혼종')을 쓴다. */
|
|
68
68
|
export declare const WORD_TYPE_LABEL: Record<string, string>;
|
|
69
69
|
/** `t` 하나만 받는 작은 꼴 — `TFunction` 을 들여오지 않아 이 모듈이 i18n 에 안 매인다 */
|
|
70
|
-
|
|
70
|
+
/** i18next `t` 와 같은 꼴 — **공개 서명(`posLabels`·`wordTypeLabels`)이 받는 자리**라 이름을 영어로 둔다
|
|
71
|
+
* (사용자 지시 2026-10-13). 안쪽 이름은 한글이어도 소비자가 보는 이름은 영어라야 한다. */
|
|
72
|
+
type Translate = (k: string, o: {
|
|
71
73
|
defaultValue: string;
|
|
72
74
|
}) => string;
|
|
73
75
|
/**
|
|
@@ -76,12 +78,12 @@ type 번역 = (k: string, o: {
|
|
|
76
78
|
* 사전에 없는 약호는 한국어 전체 표기(`POS_LABEL`)로 둔다 — 자료에 새 약호가 와도 빈칸이 안 된다.
|
|
77
79
|
* @public
|
|
78
80
|
*/
|
|
79
|
-
export declare function posLabels(t:
|
|
81
|
+
export declare function posLabels(t: Translate): Record<string, string>;
|
|
80
82
|
/**
|
|
81
83
|
* **어종 약호 → 지금 언어의 이름**(`{ '한자': 'Sino-Korean', … }`). 쓰는 법은 `posLabels` 와 같다.
|
|
82
84
|
* @public
|
|
83
85
|
*/
|
|
84
|
-
export declare function wordTypeLabels(t:
|
|
86
|
+
export declare function wordTypeLabels(t: Translate): Record<string, string>;
|
|
85
87
|
/**
|
|
86
88
|
* 약호 → i18n 열쇠 조각 표(품사·어종).
|
|
87
89
|
*
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** 마지막 입력이 키보드였으면 true(모르면 false) */
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function wasKeyboardPress(): boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DeviceForm } from '../types/device';
|
|
2
|
-
type Side = '
|
|
2
|
+
type Side = 'left' | 'right';
|
|
3
3
|
/**
|
|
4
4
|
* 보청기 기기 등록 / 이력 관리 상태를 캡슐화한 훅.
|
|
5
5
|
*/
|
|
@@ -11,22 +11,10 @@ export declare function useDeviceManagement(): {
|
|
|
11
11
|
setShowDevice: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
12
12
|
deviceDirection: Side;
|
|
13
13
|
setDeviceDirection: import("react").Dispatch<import("react").SetStateAction<Side>>;
|
|
14
|
-
devices:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
setDevices: import("react").Dispatch<import("react").SetStateAction<{
|
|
19
|
-
좌: DeviceForm | null;
|
|
20
|
-
우: DeviceForm | null;
|
|
21
|
-
}>>;
|
|
22
|
-
deviceHistory: {
|
|
23
|
-
좌: DeviceForm[];
|
|
24
|
-
우: DeviceForm[];
|
|
25
|
-
};
|
|
26
|
-
setDeviceHistory: import("react").Dispatch<import("react").SetStateAction<{
|
|
27
|
-
좌: DeviceForm[];
|
|
28
|
-
우: DeviceForm[];
|
|
29
|
-
}>>;
|
|
14
|
+
devices: Record<Side, DeviceForm | null>;
|
|
15
|
+
setDevices: import("react").Dispatch<import("react").SetStateAction<Record<Side, DeviceForm | null>>>;
|
|
16
|
+
deviceHistory: Record<Side, DeviceForm[]>;
|
|
17
|
+
setDeviceHistory: import("react").Dispatch<import("react").SetStateAction<Record<Side, DeviceForm[]>>>;
|
|
30
18
|
showDeviceList: boolean;
|
|
31
19
|
setShowDeviceList: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
32
20
|
listForSide: Side;
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useCallback, useState } from 'react';
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* `form.direction` 이 `'left'`·`'right'` 가 아니면 null — 캐스팅만 하면 `devices['엉뚱한값']` 같은 열쇠가 생긴다.
|
|
5
|
+
*
|
|
6
|
+
* **옛 값(`'좌'`·`'우'`)은 받지 않는다.** 2026-10-13 사장님 결정으로 값을 영어로 옮겼고,
|
|
7
|
+
* 너그럽게 받는 몫은 **소비자가 진다** — 말귀가 DB 와 오가는 길목 한 자리에 세워 두었다
|
|
8
|
+
* (`toDbDirection`/`fromDbDirection`). 여기서 함께 받으면 **두 자리가 같은 일을 하고**,
|
|
9
|
+
* DB 가 다 옮겨진 뒤에도 어느 쪽이 걷혔는지 아무도 모르게 된다.
|
|
10
|
+
*
|
|
11
|
+
* 걸리면 **조용히 `null`** 이다 — 터지지도 찍지도 않는다. 옛 값이 그대로 들어오면
|
|
12
|
+
* 기기가 안 골라지는 꼴로 보이니, 소비자는 **코어에 넘기기 전에** 옮겨 둔다.
|
|
13
|
+
*/
|
|
14
|
+
const toSide = (d) => (d === 'left' || d === 'right' ? d : null);
|
|
5
15
|
/**
|
|
6
16
|
* 보청기 기기 등록 / 이력 관리 상태를 캡슐화한 훅.
|
|
7
17
|
*/
|
|
@@ -10,15 +20,17 @@ const toSide = (d) => (d === '좌' || d === '우' ? d : null);
|
|
|
10
20
|
*/
|
|
11
21
|
export function useDeviceManagement() {
|
|
12
22
|
const [showDevice, setShowDevice] = useState(false);
|
|
13
|
-
const [deviceDirection, setDeviceDirection] = useState('
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
const [deviceDirection, setDeviceDirection] = useState('left');
|
|
24
|
+
/* 열쇠는 `Side`(= `DeviceSide`) **값**(`'left'`·`'right'`) — 서버의 `side` 열에 오가는 자료다.
|
|
25
|
+
이름도 값도 영어다 — 값은 2026-10-13 사장님 결정으로 옮겼다(`'좌'`·`'우'` → `'left'`·`'right'`). */
|
|
26
|
+
const [devices, setDevices] = useState({ left: null, right: null });
|
|
27
|
+
const [deviceHistory, setDeviceHistory] = useState({ left: [], right: [] });
|
|
16
28
|
const [showDeviceList, setShowDeviceList] = useState(false);
|
|
17
|
-
const [listForSide, setListForSide] = useState('
|
|
29
|
+
const [listForSide, setListForSide] = useState('left');
|
|
18
30
|
// form.direction은 각 팝업(DeviceRegistrationPopup의 방향 드롭다운, DeviceListPopup의
|
|
19
|
-
//
|
|
31
|
+
// left/right 탭)에서 사용자가 실제로 고른 값이다 — deviceDirection/listForSide는 팝업을 열 때의
|
|
20
32
|
// 초기값일 뿐이라 팝업 내부에서 방향을 바꾸면 더 이상 실제 선택과 일치하지 않는다.
|
|
21
|
-
// 방향이 비었거나 '
|
|
33
|
+
// 방향이 비었거나 'left'·'right' 밖이면 **아무것도 바꾸지 않는다** — 예전에는 검사 없이 캐스팅해
|
|
22
34
|
// devices·deviceHistory 에 없는 열쇠가 생겼고 prev[side] 가 undefined 라 이력 펼치기에서 터졌다.
|
|
23
35
|
// 팝업은 닫지 않는다(사용자가 방향을 고르고 다시 제출할 수 있게).
|
|
24
36
|
const handleDeviceSubmit = useCallback((form) => {
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
* React 가 이 꼴을 위해 내준 길이다(같은 컴포넌트를 바로 다시 그린다).
|
|
12
12
|
*
|
|
13
13
|
* ```ts
|
|
14
|
-
* usePageResetOnChange(`${검색어}|${갈래}|${
|
|
14
|
+
* usePageResetOnChange(`${검색어}|${갈래}|${sort}`, setPage)
|
|
15
15
|
* const { pageItems, totalPages } = usePagedList(걸러낸것, 쪽크기, page)
|
|
16
16
|
* ```
|
|
17
17
|
*
|
|
18
|
-
* @param
|
|
18
|
+
* @param key 조건을 하나로 이어 붙인 글 — 이것이 달라지면 첫 쪽이다
|
|
19
19
|
* @param setPage 쪽 번호를 쥔 쪽에서 준다
|
|
20
20
|
* @public
|
|
21
21
|
*/
|
|
22
|
-
export declare function usePageResetOnChange(
|
|
22
|
+
export declare function usePageResetOnChange(key: string, setPage: (n: number) => void): void;
|
|
@@ -13,18 +13,18 @@ import { useState } from 'react';
|
|
|
13
13
|
* React 가 이 꼴을 위해 내준 길이다(같은 컴포넌트를 바로 다시 그린다).
|
|
14
14
|
*
|
|
15
15
|
* ```ts
|
|
16
|
-
* usePageResetOnChange(`${검색어}|${갈래}|${
|
|
16
|
+
* usePageResetOnChange(`${검색어}|${갈래}|${sort}`, setPage)
|
|
17
17
|
* const { pageItems, totalPages } = usePagedList(걸러낸것, 쪽크기, page)
|
|
18
18
|
* ```
|
|
19
19
|
*
|
|
20
|
-
* @param
|
|
20
|
+
* @param key 조건을 하나로 이어 붙인 글 — 이것이 달라지면 첫 쪽이다
|
|
21
21
|
* @param setPage 쪽 번호를 쥔 쪽에서 준다
|
|
22
22
|
* @public
|
|
23
23
|
*/
|
|
24
|
-
export function usePageResetOnChange(
|
|
25
|
-
const [본열쇠, set본열쇠] = useState(
|
|
26
|
-
if (본열쇠 !==
|
|
27
|
-
set본열쇠(
|
|
24
|
+
export function usePageResetOnChange(key, setPage) {
|
|
25
|
+
const [본열쇠, set본열쇠] = useState(key);
|
|
26
|
+
if (본열쇠 !== key) {
|
|
27
|
+
set본열쇠(key);
|
|
28
28
|
setPage(1);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -56,26 +56,37 @@ export interface TableSortOptions<K extends string> {
|
|
|
56
56
|
* **정렬이 바뀌면 첫 쪽으로** — `useTablePaging` 의 `resetOn` 에 돌려받은 `정렬` 을 그대로 넣는다.
|
|
57
57
|
* `정렬` 은 누를 때만 새 객체가 되므로(렌더마다 새로 만들지 않는다) 그 자체가 열쇠다. 쪽을 따로 들고 있으면 `onSortChange` 에서 되돌린다.
|
|
58
58
|
*
|
|
59
|
-
* `sortRows(rows,
|
|
59
|
+
* `sortRows(rows, getValue)` 은 지금 정렬로 줄을 세운 **새 배열**을 준다(정렬이 없으면 받은 배열 그대로) — `compareCell`(숫자는 수로,
|
|
60
60
|
* 한글·영문은 사람 차례) 으로 견주고 **빈 값(null·undefined·'')은 오름·내림 모두 맨 뒤**(ListPopup 과 같은 규칙).
|
|
61
61
|
*
|
|
62
62
|
* @example
|
|
63
|
-
* const {
|
|
63
|
+
* const { sort, field, sortRows } = useTableSort<'name' | 'joined'>()
|
|
64
64
|
* const 세운줄 = useMemo(() => sortRows(rows, (r, key) => r[key]), [rows, sortRows])
|
|
65
|
-
* const { pageRows, pageSizeProps, paginationProps } = useTablePaging(세운줄,
|
|
66
|
-
* <Table fields={[
|
|
65
|
+
* const { pageRows, pageSizeProps, paginationProps } = useTablePaging(세운줄, sort) // 정렬이 바뀌면 첫 쪽
|
|
66
|
+
* <Table fields={[field('이름', 'name'), field('가입일', 'joined')]} … />
|
|
67
67
|
*
|
|
68
68
|
* @public
|
|
69
69
|
*/
|
|
70
70
|
export declare function useTableSort<K extends string>(opts?: TableSortOptions<K>): {
|
|
71
|
+
sort: {
|
|
72
|
+
key: K;
|
|
73
|
+
asc: boolean;
|
|
74
|
+
} | null;
|
|
75
|
+
field: (label: string, key: K) => {
|
|
76
|
+
label: string;
|
|
77
|
+
sortDir: "desc" | "asc" | null;
|
|
78
|
+
onClick: () => void;
|
|
79
|
+
};
|
|
80
|
+
sortRows: <R>(rows: readonly R[], getValue: (row: R, key: K) => unknown) => readonly R[];
|
|
81
|
+
/** @deprecated `sort` 를 쓴다 — 같은 값이다(2026-10-13) */
|
|
71
82
|
정렬: {
|
|
72
83
|
key: K;
|
|
73
84
|
asc: boolean;
|
|
74
85
|
} | null;
|
|
86
|
+
/** @deprecated `field` 를 쓴다 — 같은 함수다(2026-10-13) */
|
|
75
87
|
칸: (label: string, key: K) => {
|
|
76
88
|
label: string;
|
|
77
89
|
sortDir: "desc" | "asc" | null;
|
|
78
90
|
onClick: () => void;
|
|
79
91
|
};
|
|
80
|
-
sortRows: <R>(rows: readonly R[], 값: (row: R, key: K) => unknown) => readonly R[];
|
|
81
92
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { VAP_PAGE_SIZE_OPTIONS } from './useVisitAllPanel';
|
|
5
|
-
import {
|
|
5
|
+
import { compareCellBlankLast } from '../utils/textSort';
|
|
6
6
|
/**
|
|
7
7
|
* 표의 쪽넘김 — 개수 보기 · 쪽 · 이번 쪽 줄.
|
|
8
8
|
*
|
|
@@ -67,14 +67,14 @@ export function useTablePaging(rows, resetOn, opts) {
|
|
|
67
67
|
* **정렬이 바뀌면 첫 쪽으로** — `useTablePaging` 의 `resetOn` 에 돌려받은 `정렬` 을 그대로 넣는다.
|
|
68
68
|
* `정렬` 은 누를 때만 새 객체가 되므로(렌더마다 새로 만들지 않는다) 그 자체가 열쇠다. 쪽을 따로 들고 있으면 `onSortChange` 에서 되돌린다.
|
|
69
69
|
*
|
|
70
|
-
* `sortRows(rows,
|
|
70
|
+
* `sortRows(rows, getValue)` 은 지금 정렬로 줄을 세운 **새 배열**을 준다(정렬이 없으면 받은 배열 그대로) — `compareCell`(숫자는 수로,
|
|
71
71
|
* 한글·영문은 사람 차례) 으로 견주고 **빈 값(null·undefined·'')은 오름·내림 모두 맨 뒤**(ListPopup 과 같은 규칙).
|
|
72
72
|
*
|
|
73
73
|
* @example
|
|
74
|
-
* const {
|
|
74
|
+
* const { sort, field, sortRows } = useTableSort<'name' | 'joined'>()
|
|
75
75
|
* const 세운줄 = useMemo(() => sortRows(rows, (r, key) => r[key]), [rows, sortRows])
|
|
76
|
-
* const { pageRows, pageSizeProps, paginationProps } = useTablePaging(세운줄,
|
|
77
|
-
* <Table fields={[
|
|
76
|
+
* const { pageRows, pageSizeProps, paginationProps } = useTablePaging(세운줄, sort) // 정렬이 바뀌면 첫 쪽
|
|
77
|
+
* <Table fields={[field('이름', 'name'), field('가입일', 'joined')]} … />
|
|
78
78
|
*
|
|
79
79
|
* @public
|
|
80
80
|
*/
|
|
@@ -98,12 +98,23 @@ export function useTableSort(opts) {
|
|
|
98
98
|
onClick: () => 누름(key),
|
|
99
99
|
}), [정렬, 누름]);
|
|
100
100
|
/** 지금 정렬로 세운 새 배열(정렬 없으면 받은 배열 그대로). 빈 값은 오름·내림 모두 맨 뒤. */
|
|
101
|
-
const sortRows = useCallback((rows,
|
|
101
|
+
const sortRows = useCallback((rows, getValue) => {
|
|
102
102
|
if (!정렬)
|
|
103
103
|
return rows;
|
|
104
104
|
const { key, asc } = 정렬;
|
|
105
|
-
return [...rows].sort((a, b) =>
|
|
105
|
+
return [...rows].sort((a, b) => compareCellBlankLast(getValue(a, key), getValue(b, key), asc));
|
|
106
106
|
}, [정렬]);
|
|
107
|
-
/* 쪽넘김에 그대로 건넨다 — 차례가 바뀌면 첫 쪽으로
|
|
108
|
-
|
|
107
|
+
/* 쪽넘김에 그대로 건넨다 — 차례가 바뀌면 첫 쪽으로 돌아가게.
|
|
108
|
+
**영어 이름이 정본이다**(`sort`·`field`) — 공개 API 는 영어라야 한다(사용자 지시 2026-10-13).
|
|
109
|
+
한글 이름(`정렬`·`칸`)은 **같은 것을 가리키는 옛 이름**으로 남긴다: 말귀가 77 줄에서 그 이름으로
|
|
110
|
+
풀어 쓰고 있어, 한 번에 걷으면 그 저장소가 통째로 빨개진다. 그쪽이 옮긴 뒤 걷는다. */
|
|
111
|
+
return {
|
|
112
|
+
sort: 정렬,
|
|
113
|
+
field: 칸,
|
|
114
|
+
sortRows,
|
|
115
|
+
/** @deprecated `sort` 를 쓴다 — 같은 값이다(2026-10-13) */
|
|
116
|
+
정렬,
|
|
117
|
+
/** @deprecated `field` 를 쓴다 — 같은 함수다(2026-10-13) */
|
|
118
|
+
칸,
|
|
119
|
+
};
|
|
109
120
|
}
|
|
@@ -56,9 +56,11 @@ export interface WordTrainingFilterCaps {
|
|
|
56
56
|
* 등급 고르개의 설명 줄들 — 지금 언어로. 사전에 배열이 없으면 한국어 원문.
|
|
57
57
|
*
|
|
58
58
|
* **공개 API 가 아니다**(대응표에 `internal`) — 코어 안에서만 쓰고, 내보내는 것은 못이 읽기 때문이다.
|
|
59
|
+
* 그래도 **이름은 영어다** — `internal` 이라도 배포 `d.ts` 에는 그대로 나가 소비자 편집기에 보인다
|
|
60
|
+
* (사용자 지시 2026-10-13. 소비자 사용처 0 이라 옛 이름을 남기지 않았다 — 말귀의 같은 이름 함수는 제 지역 것이다).
|
|
59
61
|
* 앱은 제 열쇠(`wordSearch.filter.gradeInfo`)와 제 받는 함수를 따로 갖는다.
|
|
60
62
|
*/
|
|
61
|
-
export declare function
|
|
63
|
+
export declare function gradeDescriptions(t: TFunction): string[];
|
|
62
64
|
export declare function useWordTrainingFilter(groupKey: string, caps: WordTrainingFilterCaps, opts?: {
|
|
63
65
|
resetOnMount?: boolean;
|
|
64
66
|
resetKey?: string;
|
|
@@ -78,7 +78,7 @@ function readComposition(saved) {
|
|
|
78
78
|
* @public
|
|
79
79
|
*/
|
|
80
80
|
/** 사전이 배열을 안 주면 한국어 원문으로 물러선다 — 빈 줄을 띄우지 않는다((205)) */
|
|
81
|
-
const
|
|
81
|
+
const GRADE_DESCRIPTIONS_KO = [
|
|
82
82
|
'1등급 : 학령전기에 습득하여 평생에 걸쳐 일상 언어생활을 하는 데 사용되는 어휘',
|
|
83
83
|
'2등급 : 초등학교 1-2학년 난이도 어휘',
|
|
84
84
|
'3등급 : 초등학교 3-4학년 난이도 어휘',
|
|
@@ -89,11 +89,13 @@ const 등급설명기본 = [
|
|
|
89
89
|
* 등급 고르개의 설명 줄들 — 지금 언어로. 사전에 배열이 없으면 한국어 원문.
|
|
90
90
|
*
|
|
91
91
|
* **공개 API 가 아니다**(대응표에 `internal`) — 코어 안에서만 쓰고, 내보내는 것은 못이 읽기 때문이다.
|
|
92
|
+
* 그래도 **이름은 영어다** — `internal` 이라도 배포 `d.ts` 에는 그대로 나가 소비자 편집기에 보인다
|
|
93
|
+
* (사용자 지시 2026-10-13. 소비자 사용처 0 이라 옛 이름을 남기지 않았다 — 말귀의 같은 이름 함수는 제 지역 것이다).
|
|
92
94
|
* 앱은 제 열쇠(`wordSearch.filter.gradeInfo`)와 제 받는 함수를 따로 갖는다.
|
|
93
95
|
*/
|
|
94
|
-
export function
|
|
95
|
-
const v = t('wordPairs.filter.difficultyInfo', { returnObjects: true, defaultValue:
|
|
96
|
-
return Array.isArray(v) && v.length > 0 && v.every(x => typeof x === 'string') ? v :
|
|
96
|
+
export function gradeDescriptions(t) {
|
|
97
|
+
const v = t('wordPairs.filter.difficultyInfo', { returnObjects: true, defaultValue: GRADE_DESCRIPTIONS_KO });
|
|
98
|
+
return Array.isArray(v) && v.length > 0 && v.every(x => typeof x === 'string') ? v : GRADE_DESCRIPTIONS_KO;
|
|
97
99
|
}
|
|
98
100
|
export function useWordTrainingFilter(groupKey, caps, opts) {
|
|
99
101
|
const { t, i18n } = useTranslation();
|
|
@@ -450,7 +452,7 @@ export function useWordTrainingFilter(groupKey, caps, opts) {
|
|
|
450
452
|
사전이 배열을 안 주면(옛 사전을 덮어쓴 소비자 등) **한국어 원문으로 물러선다** — 빈 줄을
|
|
451
453
|
띄우지 않는다. 재 보고 적는다: `returnObjects` 는 배열을 그대로 주고 `defaultValue` 도
|
|
452
454
|
배열로 물러선다(i18next 26.4.1). */
|
|
453
|
-
labelInfo: infoAction(
|
|
455
|
+
labelInfo: infoAction(gradeDescriptions(t)),
|
|
454
456
|
}),
|
|
455
457
|
label: LABELS.gradeLabel,
|
|
456
458
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const isDev: boolean;
|
|
2
|
-
/**
|
|
3
|
-
export declare function devWarnOnce(
|
|
2
|
+
/** `key` 하나당 한 번만 찍는다. 운영 빌드에서는 아무 일도 하지 않는다. */
|
|
3
|
+
export declare function devWarnOnce(key: string, ...args: unknown[]): void;
|
|
4
4
|
/** 시험용 — 기록을 비운다. 운영 코드에서 부를 일은 없다. */
|
|
5
5
|
export declare function __resetDevWarnOnce(): void;
|
|
@@ -27,12 +27,12 @@ export const isDev = (() => {
|
|
|
27
27
|
return false;
|
|
28
28
|
}
|
|
29
29
|
})();
|
|
30
|
-
/**
|
|
31
|
-
export function devWarnOnce(
|
|
32
|
-
if (!isDev || 찍은것.has(
|
|
30
|
+
/** `key` 하나당 한 번만 찍는다. 운영 빌드에서는 아무 일도 하지 않는다. */
|
|
31
|
+
export function devWarnOnce(key, ...args) {
|
|
32
|
+
if (!isDev || 찍은것.has(key))
|
|
33
33
|
return;
|
|
34
|
-
찍은것.add(
|
|
35
|
-
console.warn(
|
|
34
|
+
찍은것.add(key);
|
|
35
|
+
console.warn(...args);
|
|
36
36
|
}
|
|
37
37
|
/** 시험용 — 기록을 비운다. 운영 코드에서 부를 일은 없다. */
|
|
38
38
|
export function __resetDevWarnOnce() {
|
|
@@ -11,4 +11,4 @@ import type { FilterPanelSection } from '../types/filterPanel';
|
|
|
11
11
|
* 안 담는 절: `note`(고르는 절이 아니다) · `input`(빈 칸을 '전체'라 말하면 거짓이다) ·
|
|
12
12
|
* 고른 것이 없는데 '전체' 보기도 없는 절(말할 것이 없다).
|
|
13
13
|
*/
|
|
14
|
-
export declare function buildFilterSummary(sections: readonly FilterPanelSection[], selected: Record<string, string | string[]> | undefined,
|
|
14
|
+
export declare function buildFilterSummary(sections: readonly FilterPanelSection[], selected: Record<string, string | string[]> | undefined, allLabel: string): string;
|
|
@@ -11,13 +11,13 @@ import { FILTER_ALL_VALUE } from '../types/filterPanel';
|
|
|
11
11
|
* 안 담는 절: `note`(고르는 절이 아니다) · `input`(빈 칸을 '전체'라 말하면 거짓이다) ·
|
|
12
12
|
* 고른 것이 없는데 '전체' 보기도 없는 절(말할 것이 없다).
|
|
13
13
|
*/
|
|
14
|
-
export function buildFilterSummary(sections, selected,
|
|
14
|
+
export function buildFilterSummary(sections, selected, allLabel) {
|
|
15
15
|
const 조각들 = [];
|
|
16
16
|
for (const section of sections ?? []) {
|
|
17
17
|
if (section.type === 'note' || section.type === 'input')
|
|
18
18
|
continue;
|
|
19
19
|
const 이름 = section.displayLabel ?? section.label;
|
|
20
|
-
const 값 = 값말(section, selected?.[section.label],
|
|
20
|
+
const 값 = 값말(section, selected?.[section.label], allLabel);
|
|
21
21
|
if (값 === null)
|
|
22
22
|
continue;
|
|
23
23
|
조각들.push(`${이름} ${값}`);
|
|
@@ -25,12 +25,12 @@ export function buildFilterSummary(sections, selected, 전체말) {
|
|
|
25
25
|
return 조각들.join(' · ');
|
|
26
26
|
}
|
|
27
27
|
/** 한 절이 무엇으로 정해져 있는지 — 말할 것이 없으면 `null` */
|
|
28
|
-
function 값말(section, sel,
|
|
28
|
+
function 값말(section, sel, allLabel) {
|
|
29
29
|
const 고른것 = Array.isArray(sel) ? sel : sel === undefined ? [] : [sel];
|
|
30
30
|
/* '전체' 는 두 꼴로 온다 — 아무것도 안 고른 것과 '전체' 보기를 고른 것. 같은 말로 읽는다. */
|
|
31
31
|
const 실제 = 고른것.filter(v => v !== FILTER_ALL_VALUE);
|
|
32
32
|
if (실제.length === 0) {
|
|
33
|
-
return section.allOption ? (section.allLabel ??
|
|
33
|
+
return section.allOption ? (section.allLabel ?? allLabel) : null;
|
|
34
34
|
}
|
|
35
35
|
return 실제.map(v => 보기말(section, v)).join(', ');
|
|
36
36
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RefObject } from 'react';
|
|
2
2
|
/** 소비자가 통을 안 알려 줄 때 찾아 보는 이름 — 옛 동작 유지(말귀 셸의 본문). @internal */
|
|
3
|
-
export declare const
|
|
3
|
+
export declare const DEFAULT_SCROLL_SELECTOR = ".content-area";
|
|
4
4
|
/**
|
|
5
5
|
* 갈래를 고른 뒤 본문을 맨 위로 — 내비(SideNav·BottomNav)가 함께 쓴다. @internal
|
|
6
6
|
*
|
|
@@ -8,4 +8,4 @@ export declare const 기본통고르개 = ".content-area";
|
|
|
8
8
|
* 그것이 **아무 일도 하지 않는다** — 그때는 조용히 죽는 대신 개발 모드에 한 번 알린다.
|
|
9
9
|
* 코어가 `.content-area`(소비자 클래스 이름)를 붙잡고 있던 자리라, 부르는 쪽이 통을 알려 주는 것이 정본이다.
|
|
10
10
|
*/
|
|
11
|
-
export declare function scrollNavTargetToTop(scrollContainer?: string | RefObject<HTMLElement | null>,
|
|
11
|
+
export declare function scrollNavTargetToTop(scrollContainer?: string | RefObject<HTMLElement | null>, from?: string): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { devWarnOnce } from './devWarn';
|
|
2
2
|
/** 소비자가 통을 안 알려 줄 때 찾아 보는 이름 — 옛 동작 유지(말귀 셸의 본문). @internal */
|
|
3
|
-
export const
|
|
3
|
+
export const DEFAULT_SCROLL_SELECTOR = '.content-area';
|
|
4
4
|
/**
|
|
5
5
|
* 갈래를 고른 뒤 본문을 맨 위로 — 내비(SideNav·BottomNav)가 함께 쓴다. @internal
|
|
6
6
|
*
|
|
@@ -8,18 +8,18 @@ export const 기본통고르개 = '.content-area';
|
|
|
8
8
|
* 그것이 **아무 일도 하지 않는다** — 그때는 조용히 죽는 대신 개발 모드에 한 번 알린다.
|
|
9
9
|
* 코어가 `.content-area`(소비자 클래스 이름)를 붙잡고 있던 자리라, 부르는 쪽이 통을 알려 주는 것이 정본이다.
|
|
10
10
|
*/
|
|
11
|
-
export function scrollNavTargetToTop(scrollContainer,
|
|
11
|
+
export function scrollNavTargetToTop(scrollContainer, from = 'Nav') {
|
|
12
12
|
try {
|
|
13
13
|
const el = typeof scrollContainer === 'string'
|
|
14
14
|
? document.querySelector(scrollContainer)
|
|
15
|
-
: scrollContainer?.current ?? document.querySelector(
|
|
15
|
+
: scrollContainer?.current ?? document.querySelector(DEFAULT_SCROLL_SELECTOR);
|
|
16
16
|
if (el) {
|
|
17
17
|
el.scrollTop = 0;
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
const 문서 = document.scrollingElement ?? document.documentElement;
|
|
21
21
|
if (문서 && 문서.scrollHeight <= 문서.clientHeight) {
|
|
22
|
-
devWarnOnce('nav-scroll-container', `[${
|
|
22
|
+
devWarnOnce('nav-scroll-container', `[${from}] 본문을 맨 위로 되돌릴 통을 못 찾았다(기본 ${DEFAULT_SCROLL_SELECTOR}) — 창도 구를 수 없어 아무 일도 하지 않는다. scrollContainer 로 통을 알려 준다.`);
|
|
23
23
|
}
|
|
24
24
|
window.scrollTo({ top: 0 });
|
|
25
25
|
}
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
* 진짜 `<button>` 을 쓸 수 있으면 그쪽이 낫다. 이건 이미 `<div>`·`<li>` 로 짜인
|
|
15
15
|
* 자리를 **모양을 안 바꾸고** 눌리게 만드는 길이다.
|
|
16
16
|
*/
|
|
17
|
-
export function pressable(onPress,
|
|
17
|
+
export function pressable(onPress, options) {
|
|
18
18
|
if (!onPress)
|
|
19
19
|
return {};
|
|
20
20
|
return {
|
|
21
|
-
role:
|
|
21
|
+
role: options?.role ?? 'button',
|
|
22
22
|
tabIndex: 0,
|
|
23
|
-
...(
|
|
23
|
+
...(options?.selected === undefined ? {} : { 'aria-selected': options.selected }),
|
|
24
24
|
onKeyDown: (e) => {
|
|
25
25
|
/* **이 자리 자신에서 난 키만** 받는다. 안에 든 진짜 단추(랜딩 띠의 action 등)에서 누른 Enter 가 올라오면
|
|
26
26
|
전에는 여기서 preventDefault 해 그 단추의 click 을 막고 이 자리의 onPress 를 대신 불렀다. */
|
|
@@ -20,17 +20,17 @@ export interface TabSessionLike {
|
|
|
20
20
|
* 여기서 막으면 이 탭이 로그인된 채로 남는다 — 막는 것보다 나쁜 쪽이다.
|
|
21
21
|
*
|
|
22
22
|
* ```ts
|
|
23
|
-
* onAuthStateChange((event,
|
|
23
|
+
* onAuthStateChange((event, incoming) => {
|
|
24
24
|
* getSession().then(({ data }) => {
|
|
25
|
-
* const
|
|
26
|
-
* if (!isSessionForThisTab(
|
|
27
|
-
*
|
|
25
|
+
* const thisTab = data.session
|
|
26
|
+
* if (!isSessionForThisTab(incoming, thisTab)) return // 다른 탭에서 온 것 — 무시
|
|
27
|
+
* …thisTab 으로 상태를 맞춘다
|
|
28
28
|
* })
|
|
29
29
|
* })
|
|
30
30
|
* ```
|
|
31
31
|
*
|
|
32
|
-
* @param
|
|
33
|
-
* @param
|
|
32
|
+
* @param incoming 이벤트가 실어 온 세션(없을 수 있다)
|
|
33
|
+
* @param thisTab `getSession()` 이 이 탭 저장소에서 다시 읽은 세션
|
|
34
34
|
* @public
|
|
35
35
|
*/
|
|
36
|
-
export declare function isSessionForThisTab(
|
|
36
|
+
export declare function isSessionForThisTab(incoming: TabSessionLike | null | undefined, thisTab: TabSessionLike | null | undefined): boolean;
|
|
@@ -16,21 +16,21 @@
|
|
|
16
16
|
* 여기서 막으면 이 탭이 로그인된 채로 남는다 — 막는 것보다 나쁜 쪽이다.
|
|
17
17
|
*
|
|
18
18
|
* ```ts
|
|
19
|
-
* onAuthStateChange((event,
|
|
19
|
+
* onAuthStateChange((event, incoming) => {
|
|
20
20
|
* getSession().then(({ data }) => {
|
|
21
|
-
* const
|
|
22
|
-
* if (!isSessionForThisTab(
|
|
23
|
-
*
|
|
21
|
+
* const thisTab = data.session
|
|
22
|
+
* if (!isSessionForThisTab(incoming, thisTab)) return // 다른 탭에서 온 것 — 무시
|
|
23
|
+
* …thisTab 으로 상태를 맞춘다
|
|
24
24
|
* })
|
|
25
25
|
* })
|
|
26
26
|
* ```
|
|
27
27
|
*
|
|
28
|
-
* @param
|
|
29
|
-
* @param
|
|
28
|
+
* @param incoming 이벤트가 실어 온 세션(없을 수 있다)
|
|
29
|
+
* @param thisTab `getSession()` 이 이 탭 저장소에서 다시 읽은 세션
|
|
30
30
|
* @public
|
|
31
31
|
*/
|
|
32
|
-
export function isSessionForThisTab(
|
|
33
|
-
if (
|
|
32
|
+
export function isSessionForThisTab(incoming, thisTab) {
|
|
33
|
+
if (!incoming)
|
|
34
34
|
return true;
|
|
35
|
-
return
|
|
35
|
+
return !!thisTab && thisTab.access_token === incoming.access_token;
|
|
36
36
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** 글자 둘을 한국어 차례로 견준다 */
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function compareKorean(a: string, b: string): number;
|
|
3
3
|
/**
|
|
4
4
|
* 표의 한 칸끼리 견준다 — **숫자는 숫자로, 글자 속 숫자도 수로.**
|
|
5
5
|
*
|
|
@@ -13,10 +13,20 @@ export declare function 한글순(a: string, b: string): number;
|
|
|
13
13
|
* 비어 있는 값(`null`·`undefined`·NaN)은 빈 글자로 본다 — 부르는 쪽이 이미
|
|
14
14
|
* `?? ''` 로 채워 넘기던 것과 같은 뜻이다.
|
|
15
15
|
*/
|
|
16
|
-
export declare function
|
|
16
|
+
export declare function compareCell(a: unknown, b: unknown): number;
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* @deprecated **`compareCell` 을 쓴다 — 같은 함수다.**
|
|
19
|
+
*
|
|
20
|
+
* 2026-10-13 공개 이름을 영어로 옮기며(사장님 지시) 옛 이름을 남겼다. 말귀가
|
|
21
|
+
* `import { 칸순 } from '@ihabdevteam/core/utils/textSort'` 를 **다섯 줄**에서
|
|
22
|
+
* 쓰고 있어 한 번에 걷으면 그쪽이 통째로 빨개진다. 옮긴 뒤 알려 주면 걷는다.
|
|
23
|
+
*
|
|
24
|
+
* 같은 꼴로 남겨 둔 것이 또 있다 — `useTableSort()` 의 `정렬`·`칸`.
|
|
25
|
+
*/
|
|
26
|
+
export declare const 칸순: typeof compareCell;
|
|
27
|
+
/**
|
|
28
|
+
* 표 정렬 한 번의 비교 — compareCell에 방향을 입히되 **빈 값은 오름·내림 모두 맨 뒤.**
|
|
29
|
+
* 방향을 `-compareCell` 으로 뒤집기만 하면 빈 값이 오름차순 맨 앞·내림차순 맨 뒤로 오가, 표마다 빈 줄이 다른 끝에 섰다.
|
|
20
30
|
* ListPopup 과 useTableSort().sortRows 가 함께 쓴다.
|
|
21
31
|
*/
|
|
22
|
-
export declare function
|
|
32
|
+
export declare function compareCellBlankLast(a: unknown, b: unknown, asc: boolean): number;
|