@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
|
@@ -1678,7 +1678,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1678
1678
|
*
|
|
1679
1679
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1680
1680
|
*/
|
|
1681
|
-
export declare const CORE_VERSION = "0.
|
|
1681
|
+
export declare const CORE_VERSION = "0.322.0";
|
|
1682
1682
|
|
|
1683
1683
|
/** @public */
|
|
1684
1684
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -2039,7 +2039,7 @@ export declare interface DeviceRow {
|
|
|
2039
2039
|
worn: boolean;
|
|
2040
2040
|
}
|
|
2041
2041
|
|
|
2042
|
-
declare type DeviceSide = '
|
|
2042
|
+
declare type DeviceSide = 'left' | 'right';
|
|
2043
2043
|
|
|
2044
2044
|
export declare function DeviceSideTable({ rows, loading, onSelect, onDelete, onWear, onRelease, onAdd }: DeviceSideTableProps): JSX.Element;
|
|
2045
2045
|
|
|
@@ -2231,10 +2231,10 @@ export declare interface DropdownProps {
|
|
|
2231
2231
|
* @public 소음이 **들리는 크기**(0~2, 원본 소음 파일 기준) = mainVolume × 비율. 켜짐 여부는 보지
|
|
2232
2232
|
* 않는다. 결과 기록·파형이 쓴다. 소음 `<audio>` 에 거는 값은 `noiseElementVolume`.
|
|
2233
2233
|
*/
|
|
2234
|
-
export declare function effectiveNoiseVolume(settings:
|
|
2234
|
+
export declare function effectiveNoiseVolume(settings: SpeechVolumeSettings): number;
|
|
2235
2235
|
|
|
2236
2236
|
/** @public 문제 소리 볼륨(0~1) = mainVolume. 비율과 상관없다. */
|
|
2237
|
-
export declare function effectiveSpeechVolume(settings:
|
|
2237
|
+
export declare function effectiveSpeechVolume(settings: SpeechVolumeSettings): number;
|
|
2238
2238
|
|
|
2239
2239
|
/** @public */
|
|
2240
2240
|
export declare function EffectPartyPopper({ active, count, duration, strength, // multiplier for speed/spread
|
|
@@ -2927,6 +2927,16 @@ export declare function getTTSUrlResolver(): ((text: string) => string | Promise
|
|
|
2927
2927
|
/** @public 단어 풀 조회 */
|
|
2928
2928
|
export declare function getWordPool(): WordPool | null;
|
|
2929
2929
|
|
|
2930
|
+
/**
|
|
2931
|
+
* 등급 고르개의 설명 줄들 — 지금 언어로. 사전에 배열이 없으면 한국어 원문.
|
|
2932
|
+
*
|
|
2933
|
+
* **공개 API 가 아니다**(대응표에 `internal`) — 코어 안에서만 쓰고, 내보내는 것은 못이 읽기 때문이다.
|
|
2934
|
+
* 그래도 **이름은 영어다** — `internal` 이라도 배포 `d.ts` 에는 그대로 나가 소비자 편집기에 보인다
|
|
2935
|
+
* (사용자 지시 2026-10-13. 소비자 사용처 0 이라 옛 이름을 남기지 않았다 — 말귀의 같은 이름 함수는 제 지역 것이다).
|
|
2936
|
+
* 앱은 제 열쇠(`wordSearch.filter.gradeInfo`)와 제 받는 함수를 따로 갖는다.
|
|
2937
|
+
*/
|
|
2938
|
+
export declare function gradeDescriptions(t: TFunction): string[];
|
|
2939
|
+
|
|
2930
2940
|
/** @public 이 타입에 '문제(정답) 단어' 구조 제약이 있는가(편집기 카운트/후보 좁히기 판단용). */
|
|
2931
2941
|
export declare function hasQuestionWordConstraint(typeKey: WordTrainingTypeKey): boolean;
|
|
2932
2942
|
|
|
@@ -3179,20 +3189,20 @@ declare interface IsSelectionCorrectParams {
|
|
|
3179
3189
|
* 여기서 막으면 이 탭이 로그인된 채로 남는다 — 막는 것보다 나쁜 쪽이다.
|
|
3180
3190
|
*
|
|
3181
3191
|
* ```ts
|
|
3182
|
-
* onAuthStateChange((event,
|
|
3192
|
+
* onAuthStateChange((event, incoming) => {
|
|
3183
3193
|
* getSession().then(({ data }) => {
|
|
3184
|
-
* const
|
|
3185
|
-
* if (!isSessionForThisTab(
|
|
3186
|
-
*
|
|
3194
|
+
* const thisTab = data.session
|
|
3195
|
+
* if (!isSessionForThisTab(incoming, thisTab)) return // 다른 탭에서 온 것 — 무시
|
|
3196
|
+
* …thisTab 으로 상태를 맞춘다
|
|
3187
3197
|
* })
|
|
3188
3198
|
* })
|
|
3189
3199
|
* ```
|
|
3190
3200
|
*
|
|
3191
|
-
* @param
|
|
3192
|
-
* @param
|
|
3201
|
+
* @param incoming 이벤트가 실어 온 세션(없을 수 있다)
|
|
3202
|
+
* @param thisTab `getSession()` 이 이 탭 저장소에서 다시 읽은 세션
|
|
3193
3203
|
* @public
|
|
3194
3204
|
*/
|
|
3195
|
-
export declare function isSessionForThisTab(
|
|
3205
|
+
export declare function isSessionForThisTab(incoming: TabSessionLike | null | undefined, thisTab: TabSessionLike | null | undefined): boolean;
|
|
3196
3206
|
|
|
3197
3207
|
/**
|
|
3198
3208
|
* @public 문자열이 음절 패턴 DSL 문법인지 — 쉼표(,) 포함 여부로 판별한다.
|
|
@@ -3991,7 +4001,7 @@ declare interface NoiseContext_2 {
|
|
|
3991
4001
|
* @public 소음 `<audio>` 볼륨(0~1) = min(1, 들리는 크기 ÷ gain). gain 은 소음 파일이 원본보다 몇 배
|
|
3992
4002
|
* 큰지 — 기본은 `configureStorage({ noiseFileGain })`(없으면 1).
|
|
3993
4003
|
*/
|
|
3994
|
-
export declare function noiseElementVolume(settings:
|
|
4004
|
+
export declare function noiseElementVolume(settings: SpeechVolumeSettings, gain?: number): number;
|
|
3995
4005
|
|
|
3996
4006
|
/** @public */
|
|
3997
4007
|
export declare const NoisePlayerContext: Context<NoisePlayerContextValue | null>;
|
|
@@ -4379,10 +4389,8 @@ declare type PanelRowDescription = {
|
|
|
4379
4389
|
export declare function PanelRowDevice({ devices, onRelease, emptyLabel }: PanelRowDeviceProps): JSX.Element;
|
|
4380
4390
|
|
|
4381
4391
|
export declare interface PanelRowDeviceProps {
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
우: DeviceEntry | null;
|
|
4385
|
-
};
|
|
4392
|
+
/** 쪽별로 지금 끼고 있는 기기 — 열쇠는 `DeviceSide` 값이다(`HearingDeviceInfo` 와 같은 꼴) */
|
|
4393
|
+
devices: Record<DeviceSide, DeviceEntry | null>;
|
|
4386
4394
|
/** 있으면 각 쪽 이름 옆에 '착용 해제'가 선다 — 그 쪽의 '지금 착용중' 만 내린다(이력은 남는다) */
|
|
4387
4395
|
onRelease?: (side: DeviceSide) => void;
|
|
4388
4396
|
/**
|
|
@@ -5287,7 +5295,7 @@ export declare const POS_LABEL: Record<string, string>;
|
|
|
5287
5295
|
* 사전에 없는 약호는 한국어 전체 표기(`POS_LABEL`)로 둔다 — 자료에 새 약호가 와도 빈칸이 안 된다.
|
|
5288
5296
|
* @public
|
|
5289
5297
|
*/
|
|
5290
|
-
export declare function posLabels(t:
|
|
5298
|
+
export declare function posLabels(t: Translate): Record<string, string>;
|
|
5291
5299
|
|
|
5292
5300
|
/**
|
|
5293
5301
|
* **늦게 받는 화면 청크를 한가할 때 미리 받아 둔다.**
|
|
@@ -6754,7 +6762,7 @@ export declare type ShuffledItem = {
|
|
|
6754
6762
|
text: string;
|
|
6755
6763
|
};
|
|
6756
6764
|
|
|
6757
|
-
declare type Side = '
|
|
6765
|
+
declare type Side = 'left' | 'right';
|
|
6758
6766
|
|
|
6759
6767
|
/** @public */
|
|
6760
6768
|
export declare function SideNav(props: SideNavProps): React_2.JSX.Element;
|
|
@@ -7108,6 +7116,13 @@ export declare interface SpeakerPos {
|
|
|
7108
7116
|
/** @public */
|
|
7109
7117
|
export declare type SpeakerState = 'idle' | 'active' | 'played' | 'correct' | 'wrong';
|
|
7110
7118
|
|
|
7119
|
+
/** `effectiveSpeechVolume`·`effectiveNoiseVolume`·`noiseElementVolume` 이 받는 것 —
|
|
7120
|
+
* **공개 `d.ts` 에 이름이 그대로 나가는 자리**라 영어로 둔다. 안쪽 이름은 한글로 쓰는 저장소지만,
|
|
7121
|
+
* 소비자가 편집기에서 보는 이름은 영어라야 한다(사용자 지시 2026-10-13). */
|
|
7122
|
+
declare type SpeechVolumeSettings = Pick<CoreNoiseSettings, 'level' | 'mainVolume'> & {
|
|
7123
|
+
noiseRatio?: number;
|
|
7124
|
+
};
|
|
7125
|
+
|
|
7111
7126
|
/**
|
|
7112
7127
|
* 앱에 처음 들어올 때 로고를 한 번 보여 주는 연출.
|
|
7113
7128
|
*
|
|
@@ -9525,6 +9540,13 @@ export declare interface TrainingWordCardProps {
|
|
|
9525
9540
|
orderRank?: number | null;
|
|
9526
9541
|
}
|
|
9527
9542
|
|
|
9543
|
+
/** `t` 하나만 받는 작은 꼴 — `TFunction` 을 들여오지 않아 이 모듈이 i18n 에 안 매인다 */
|
|
9544
|
+
/** i18next `t` 와 같은 꼴 — **공개 서명(`posLabels`·`wordTypeLabels`)이 받는 자리**라 이름을 영어로 둔다
|
|
9545
|
+
* (사용자 지시 2026-10-13). 안쪽 이름은 한글이어도 소비자가 보는 이름은 영어라야 한다. */
|
|
9546
|
+
declare type Translate = (k: string, o: {
|
|
9547
|
+
defaultValue: string;
|
|
9548
|
+
}) => string;
|
|
9549
|
+
|
|
9528
9550
|
declare type TranslateFn = TFunction;
|
|
9529
9551
|
|
|
9530
9552
|
/**
|
|
@@ -9770,22 +9792,10 @@ export declare function useDeviceManagement(): {
|
|
|
9770
9792
|
setShowDevice: Dispatch<SetStateAction<boolean>>;
|
|
9771
9793
|
deviceDirection: Side;
|
|
9772
9794
|
setDeviceDirection: Dispatch<SetStateAction<Side>>;
|
|
9773
|
-
devices:
|
|
9774
|
-
|
|
9775
|
-
|
|
9776
|
-
|
|
9777
|
-
setDevices: Dispatch<SetStateAction< {
|
|
9778
|
-
좌: DeviceForm | null;
|
|
9779
|
-
우: DeviceForm | null;
|
|
9780
|
-
}>>;
|
|
9781
|
-
deviceHistory: {
|
|
9782
|
-
좌: DeviceForm[];
|
|
9783
|
-
우: DeviceForm[];
|
|
9784
|
-
};
|
|
9785
|
-
setDeviceHistory: Dispatch<SetStateAction< {
|
|
9786
|
-
좌: DeviceForm[];
|
|
9787
|
-
우: DeviceForm[];
|
|
9788
|
-
}>>;
|
|
9795
|
+
devices: Record<Side, DeviceForm | null>;
|
|
9796
|
+
setDevices: Dispatch<SetStateAction<Record<Side, DeviceForm | null>>>;
|
|
9797
|
+
deviceHistory: Record<Side, DeviceForm[]>;
|
|
9798
|
+
setDeviceHistory: Dispatch<SetStateAction<Record<Side, DeviceForm[]>>>;
|
|
9789
9799
|
showDeviceList: boolean;
|
|
9790
9800
|
setShowDeviceList: Dispatch<SetStateAction<boolean>>;
|
|
9791
9801
|
listForSide: Side;
|
|
@@ -9995,15 +10005,15 @@ export declare function usePagedList<T>(items: T[], perPage: number, page: numbe
|
|
|
9995
10005
|
* React 가 이 꼴을 위해 내준 길이다(같은 컴포넌트를 바로 다시 그린다).
|
|
9996
10006
|
*
|
|
9997
10007
|
* ```ts
|
|
9998
|
-
* usePageResetOnChange(`${검색어}|${갈래}|${
|
|
10008
|
+
* usePageResetOnChange(`${검색어}|${갈래}|${sort}`, setPage)
|
|
9999
10009
|
* const { pageItems, totalPages } = usePagedList(걸러낸것, 쪽크기, page)
|
|
10000
10010
|
* ```
|
|
10001
10011
|
*
|
|
10002
|
-
* @param
|
|
10012
|
+
* @param key 조건을 하나로 이어 붙인 글 — 이것이 달라지면 첫 쪽이다
|
|
10003
10013
|
* @param setPage 쪽 번호를 쥔 쪽에서 준다
|
|
10004
10014
|
* @public
|
|
10005
10015
|
*/
|
|
10006
|
-
export declare function usePageResetOnChange(
|
|
10016
|
+
export declare function usePageResetOnChange(key: string, setPage: (n: number) => void): void;
|
|
10007
10017
|
|
|
10008
10018
|
/**
|
|
10009
10019
|
* 비밀번호 변경 폼 상태와 핸들러를 캡슐화한 훅.
|
|
@@ -10108,28 +10118,39 @@ export declare function useTablePaging<T>(rows: readonly T[], resetOn?: unknown,
|
|
|
10108
10118
|
* **정렬이 바뀌면 첫 쪽으로** — `useTablePaging` 의 `resetOn` 에 돌려받은 `정렬` 을 그대로 넣는다.
|
|
10109
10119
|
* `정렬` 은 누를 때만 새 객체가 되므로(렌더마다 새로 만들지 않는다) 그 자체가 열쇠다. 쪽을 따로 들고 있으면 `onSortChange` 에서 되돌린다.
|
|
10110
10120
|
*
|
|
10111
|
-
* `sortRows(rows,
|
|
10121
|
+
* `sortRows(rows, getValue)` 은 지금 정렬로 줄을 세운 **새 배열**을 준다(정렬이 없으면 받은 배열 그대로) — `compareCell`(숫자는 수로,
|
|
10112
10122
|
* 한글·영문은 사람 차례) 으로 견주고 **빈 값(null·undefined·'')은 오름·내림 모두 맨 뒤**(ListPopup 과 같은 규칙).
|
|
10113
10123
|
*
|
|
10114
10124
|
* @example
|
|
10115
|
-
* const {
|
|
10125
|
+
* const { sort, field, sortRows } = useTableSort<'name' | 'joined'>()
|
|
10116
10126
|
* const 세운줄 = useMemo(() => sortRows(rows, (r, key) => r[key]), [rows, sortRows])
|
|
10117
|
-
* const { pageRows, pageSizeProps, paginationProps } = useTablePaging(세운줄,
|
|
10118
|
-
* <Table fields={[
|
|
10127
|
+
* const { pageRows, pageSizeProps, paginationProps } = useTablePaging(세운줄, sort) // 정렬이 바뀌면 첫 쪽
|
|
10128
|
+
* <Table fields={[field('이름', 'name'), field('가입일', 'joined')]} … />
|
|
10119
10129
|
*
|
|
10120
10130
|
* @public
|
|
10121
10131
|
*/
|
|
10122
10132
|
export declare function useTableSort<K extends string>(opts?: TableSortOptions<K>): {
|
|
10133
|
+
sort: {
|
|
10134
|
+
key: K;
|
|
10135
|
+
asc: boolean;
|
|
10136
|
+
} | null;
|
|
10137
|
+
field: (label: string, key: K) => {
|
|
10138
|
+
label: string;
|
|
10139
|
+
sortDir: "desc" | "asc" | null;
|
|
10140
|
+
onClick: () => void;
|
|
10141
|
+
};
|
|
10142
|
+
sortRows: <R>(rows: readonly R[], getValue: (row: R, key: K) => unknown) => readonly R[];
|
|
10143
|
+
/** @deprecated `sort` 를 쓴다 — 같은 값이다(2026-10-13) */
|
|
10123
10144
|
정렬: {
|
|
10124
10145
|
key: K;
|
|
10125
10146
|
asc: boolean;
|
|
10126
10147
|
} | null;
|
|
10148
|
+
/** @deprecated `field` 를 쓴다 — 같은 함수다(2026-10-13) */
|
|
10127
10149
|
칸: (label: string, key: K) => {
|
|
10128
10150
|
label: string;
|
|
10129
10151
|
sortDir: "desc" | "asc" | null;
|
|
10130
10152
|
onClick: () => void;
|
|
10131
10153
|
};
|
|
10132
|
-
sortRows: <R>(rows: readonly R[], 값: (row: R, key: K) => unknown) => readonly R[];
|
|
10133
10154
|
};
|
|
10134
10155
|
|
|
10135
10156
|
/**
|
|
@@ -11601,7 +11622,7 @@ export declare type WordTrainingTypeKey = 'SAME_SOUND_PICK' | 'SAME_OR_DIFF' | '
|
|
|
11601
11622
|
* **어종 약호 → 지금 언어의 이름**(`{ '한자': 'Sino-Korean', … }`). 쓰는 법은 `posLabels` 와 같다.
|
|
11602
11623
|
* @public
|
|
11603
11624
|
*/
|
|
11604
|
-
export declare function wordTypeLabels(t:
|
|
11625
|
+
export declare function wordTypeLabels(t: Translate): Record<string, string>;
|
|
11605
11626
|
|
|
11606
11627
|
/**
|
|
11607
11628
|
* @public 틀린 문항만 — 음소별 오류를 세는 쪽이 먼저 볼 것이다.
|
|
@@ -11610,21 +11631,4 @@ export declare function wordTypeLabels(t: 번역): Record<string, string>;
|
|
|
11610
11631
|
*/
|
|
11611
11632
|
export declare function wrongItems(items: readonly TrainingItemResult[]): TrainingItemResult[];
|
|
11612
11633
|
|
|
11613
|
-
/**
|
|
11614
|
-
* 등급 고르개의 설명 줄들 — 지금 언어로. 사전에 배열이 없으면 한국어 원문.
|
|
11615
|
-
*
|
|
11616
|
-
* **공개 API 가 아니다**(대응표에 `internal`) — 코어 안에서만 쓰고, 내보내는 것은 못이 읽기 때문이다.
|
|
11617
|
-
* 앱은 제 열쇠(`wordSearch.filter.gradeInfo`)와 제 받는 함수를 따로 갖는다.
|
|
11618
|
-
*/
|
|
11619
|
-
export declare function 등급설명(t: TFunction): string[];
|
|
11620
|
-
|
|
11621
|
-
/** `t` 하나만 받는 작은 꼴 — `TFunction` 을 들여오지 않아 이 모듈이 i18n 에 안 매인다 */
|
|
11622
|
-
declare type 번역 = (k: string, o: {
|
|
11623
|
-
defaultValue: string;
|
|
11624
|
-
}) => string;
|
|
11625
|
-
|
|
11626
|
-
declare type 비율설정 = Pick<CoreNoiseSettings, 'level' | 'mainVolume'> & {
|
|
11627
|
-
noiseRatio?: number;
|
|
11628
|
-
};
|
|
11629
|
-
|
|
11630
11634
|
export { }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import './HearingDeviceInfo.css';
|
|
2
|
-
export type DeviceSide = '
|
|
2
|
+
export type DeviceSide = 'left' | 'right';
|
|
3
3
|
export interface DeviceEntry {
|
|
4
4
|
brand: string;
|
|
5
5
|
device: string;
|
|
@@ -10,10 +10,10 @@ export interface DeviceEntry {
|
|
|
10
10
|
wornDay: string;
|
|
11
11
|
}
|
|
12
12
|
interface HearingDeviceInfoProps {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
/** 쪽별로 지금 끼고 있는 기기 — 열쇠는 `DeviceSide` **값**(`'left'`·`'right'`)이다.
|
|
14
|
+
* 그 값은 서버의 `side` 열과 그대로 오가는 **자료**라 건드리지 않았고, 공개 자리에 보이는
|
|
15
|
+
* **모양만** 영어 이름으로 돌렸다(`Record`). 값까지 바꾸는 것은 DB 결정이다(사용자 지시 2026-10-13). */
|
|
16
|
+
devices: Record<DeviceSide, DeviceEntry | null>;
|
|
17
17
|
onAddDevice: (side: DeviceSide) => void;
|
|
18
18
|
onOpenList: (side: DeviceSide) => void;
|
|
19
19
|
onRemoveDevice: (side: DeviceSide) => void;
|
|
@@ -6,9 +6,9 @@ import { formatWornDate } from '../utils/wornDate';
|
|
|
6
6
|
import './HearingDeviceInfo.css';
|
|
7
7
|
export default function HearingDeviceInfo({ devices, onAddDevice, onOpenList, onRemoveDevice }) {
|
|
8
8
|
const { t, i18n } = useTranslation(); /* i18n.language — 착용일이 보는 말을 따른다((118)) */
|
|
9
|
-
return (_jsxs("div", { className: "hearing-device-info", children: [_jsx("div", { className: "hearing-device-info__title body-3", children: t('customers.device.title', '청각재활기기') }), _jsx("div", { className: "hearing-device-info__row", children: ['
|
|
9
|
+
return (_jsxs("div", { className: "hearing-device-info", children: [_jsx("div", { className: "hearing-device-info__title body-3", children: t('customers.device.title', '청각재활기기') }), _jsx("div", { className: "hearing-device-info__row", children: ['left', 'right'].map(side => {
|
|
10
10
|
const d = devices[side];
|
|
11
|
-
const sideLabel = side === '
|
|
11
|
+
const sideLabel = side === 'left' ? t('device.direction.left', '좌') : t('device.direction.right', '우');
|
|
12
12
|
return (_jsxs("div", { className: "hearing-device-info__side", children: [_jsx("span", { className: "hearing-device-info__side-label body-3", children: sideLabel }), d ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "hearing-device-info__device", children: [_jsx("span", { className: "hearing-device-info__device-name body-3", children: d.brand }), _jsxs("span", { className: "hearing-device-info__device-detail", children: [d.device, d.fitType ? ` · ${d.fitType}` : ''] }), d.productName && _jsx("span", { className: "hearing-device-info__device-product", children: d.productName }), formatWornDate(d.wornYear, d.wornMonth, d.wornDay, i18n.language) && _jsx("span", { className: "hearing-device-info__device-date", children: formatWornDate(d.wornYear, d.wornMonth, d.wornDay, i18n.language) })] }), _jsxs("div", { className: "hearing-device-info__actions", children: [_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "plus", onClick: () => onAddDevice(side), showTooltipInfo: t('device.addNew', '새 기기 추가'), ariaLabel: t('device.addNew', '새 기기 추가') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "list", onClick: () => onOpenList(side), showTooltipInfo: t('device.selectFromList', '목록에서 선택'), ariaLabel: t('device.selectFromList', '목록에서 선택') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "danger", onClick: () => onRemoveDevice(side), showTooltipInfo: t('device.removeDevice', '착용 해제'), ariaLabel: t('device.removeDevice', '착용 해제') })] })] })) : (_jsxs("div", { className: "hearing-device-info__actions", children: [_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "plus", onClick: () => onAddDevice(side), showTooltipInfo: t('device.addNew', '새 기기 추가'), ariaLabel: t('device.addNew', '새 기기 추가') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "list", onClick: () => onOpenList(side), showTooltipInfo: t('device.selectFromList', '목록에서 선택'), ariaLabel: t('device.selectFromList', '목록에서 선택') })] }))] }, side));
|
|
13
13
|
}) })] }));
|
|
14
14
|
}
|
|
@@ -153,10 +153,8 @@ export type PanelRowProps = PanelRowToggle | PanelRowSegmented | PanelRowDropdow
|
|
|
153
153
|
export declare function PanelRow(props: PanelRowProps): import("react").JSX.Element;
|
|
154
154
|
export type { DeviceSide };
|
|
155
155
|
export interface PanelRowDeviceProps {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
우: DeviceEntry | null;
|
|
159
|
-
};
|
|
156
|
+
/** 쪽별로 지금 끼고 있는 기기 — 열쇠는 `DeviceSide` 값이다(`HearingDeviceInfo` 와 같은 꼴) */
|
|
157
|
+
devices: Record<DeviceSide, DeviceEntry | null>;
|
|
160
158
|
/** 있으면 각 쪽 이름 옆에 '착용 해제'가 선다 — 그 쪽의 '지금 착용중' 만 내린다(이력은 남는다) */
|
|
161
159
|
onRelease?: (side: DeviceSide) => void;
|
|
162
160
|
/**
|
|
@@ -152,10 +152,10 @@ function 메타(d) {
|
|
|
152
152
|
}
|
|
153
153
|
export function PanelRowDevice({ devices, onRelease, emptyLabel }) {
|
|
154
154
|
const { t, i18n } = useTranslation(); /* i18n.language — 착용일이 보는 말을 따른다((118)) */
|
|
155
|
-
const SIDES = ['
|
|
155
|
+
const SIDES = ['left', 'right'];
|
|
156
156
|
return (_jsx("div", { className: "panel-row panel-row--device", children: SIDES.map(side => {
|
|
157
157
|
const d = devices[side];
|
|
158
|
-
const sideLabel = side === '
|
|
158
|
+
const sideLabel = side === 'left' ? t('device.direction.left', '좌') : t('device.direction.right', '우');
|
|
159
159
|
return (_jsxs("div", { className: "panel-row__device-column", children: [_jsxs("div", { className: "panel-row__device-head", children: [_jsx("span", { className: "panel-row__device-side body-2b", children: sideLabel }), d && onRelease && (
|
|
160
160
|
/* 표의 줄 끝 단추와 같은 모양·같은 말 — 하는 일도 같다.
|
|
161
161
|
이력에서 지우는 것이 아니라 '지금 끼고 있음' 만 내린다
|
|
@@ -3,14 +3,10 @@ import type { DeviceSide } from '../Panel';
|
|
|
3
3
|
import type { DeviceForm } from '../../types/device';
|
|
4
4
|
import './DeviceListPopup.css';
|
|
5
5
|
interface DeviceListPopupProps {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
deviceHistory: {
|
|
11
|
-
좌: DeviceEntry[];
|
|
12
|
-
우: DeviceEntry[];
|
|
13
|
-
};
|
|
6
|
+
/** 쪽별로 지금 끼고 있는 기기 — 열쇠는 `DeviceSide` 값이다 */
|
|
7
|
+
devices: Record<DeviceSide, DeviceEntry | null>;
|
|
8
|
+
/** 쪽별로 바꿔 온 이력 */
|
|
9
|
+
deviceHistory: Record<DeviceSide, DeviceEntry[]>;
|
|
14
10
|
onClose: () => void;
|
|
15
11
|
onSelect: (d: DeviceForm) => void;
|
|
16
12
|
onDelete: (side: DeviceSide, index: number) => void;
|
|
@@ -16,7 +16,7 @@ import './DeviceListPopup.css';
|
|
|
16
16
|
*/
|
|
17
17
|
export default function DeviceListPopup({ devices, deviceHistory, onClose, onSelect, onDelete }) {
|
|
18
18
|
const { t } = useTranslation();
|
|
19
|
-
const rows = useMemo(() => ['
|
|
19
|
+
const rows = useMemo(() => ['left', 'right'].flatMap(side => deviceHistory[side].map((d, index) => ({
|
|
20
20
|
index, side,
|
|
21
21
|
brand: d.brand, device: d.device,
|
|
22
22
|
fitType: d.fitType ?? '', productName: d.productName ?? '',
|
|
@@ -17,7 +17,7 @@ function 기기종류이름(device, t) {
|
|
|
17
17
|
return k ? t(`device.type.${k}`, { defaultValue: device }) : device;
|
|
18
18
|
}
|
|
19
19
|
import { useTablePaging, useTableSort } from '../../hooks/useTablePaging';
|
|
20
|
-
import {
|
|
20
|
+
import { compareCellBlankLast } from '../../utils/textSort';
|
|
21
21
|
import './DeviceListPopup.css';
|
|
22
22
|
/**
|
|
23
23
|
* 정렬에 쓸 문자열. 날짜는 **자리수를 채워** 잰다 — '2026.5.2' 와 '2026.11.5' 를
|
|
@@ -31,7 +31,7 @@ function 정렬값(r, key) {
|
|
|
31
31
|
}
|
|
32
32
|
export default function DeviceSideTable({ rows, loading, onSelect, onDelete, onWear, onRelease, onAdd }) {
|
|
33
33
|
const { t, i18n } = useTranslation(); /* i18n.language — 착용일이 보는 말을 따른다((118)) */
|
|
34
|
-
const 방향이름 = (side) => side === '
|
|
34
|
+
const 방향이름 = (side) => side === 'left' ? t('device.direction.left', '좌') : t('device.direction.right', '우');
|
|
35
35
|
/* 머리행을 누르면 그 칸으로 정렬한다 — 같은 칸을 다시 누르면 방향이 뒤집힌다.
|
|
36
36
|
쪽넘김보다 **앞에서** 정렬한다(훅에 이미 정렬된 줄을 건넨다). */
|
|
37
37
|
const { 정렬, 칸 } = useTableSort();
|
|
@@ -44,7 +44,7 @@ export default function DeviceSideTable({ rows, loading, onSelect, onDelete, onW
|
|
|
44
44
|
const cmp = Number(b.worn) - Number(a.worn); // 켜진 것이 먼저
|
|
45
45
|
return asc ? cmp : -cmp;
|
|
46
46
|
}
|
|
47
|
-
return
|
|
47
|
+
return compareCellBlankLast(정렬값(a, key), 정렬값(b, key), asc); // 빈 칸(착용일 없음 등)은 오름·내림 모두 맨 뒤 — useTableSort 와 같은 규칙
|
|
48
48
|
});
|
|
49
49
|
}, [rows, 정렬]);
|
|
50
50
|
/* 개수 보기·쪽넘김은 방문 표와 같은 것을 쓴다 — 같은 자리에서 같은 뜻으로 읽히게 */
|
|
@@ -6,7 +6,7 @@ import Popup from './Popup';
|
|
|
6
6
|
import Table from '../Table';
|
|
7
7
|
import Toolbar from '../Toolbar';
|
|
8
8
|
import './ListPopup.css';
|
|
9
|
-
import {
|
|
9
|
+
import { compareCellBlankLast } from '../../utils/textSort';
|
|
10
10
|
import { cx } from '../../utils/cx';
|
|
11
11
|
/** @public */
|
|
12
12
|
export default function ListPopup(props) {
|
|
@@ -48,9 +48,9 @@ export default function ListPopup(props) {
|
|
|
48
48
|
return 0;
|
|
49
49
|
/* `<`·`>` 로 견주면 코드포인트 차례가 된다 — 한글끼리는 맞아떨어져 안 드러나다가
|
|
50
50
|
**한글과 영문이 섞이면** 어긋난다(라틴이 한글보다 앞이라 `fwef` 가
|
|
51
|
-
`검수테스트2` 위에 선다). 숫자 칸은 숫자로 견주는 것을
|
|
51
|
+
`검수테스트2` 위에 선다). 숫자 칸은 숫자로 견주는 것을 `compareCell` 이 지킨다.
|
|
52
52
|
빈 값(null·undefined·'')은 오름·내림 모두 맨 뒤 — 전에는 '' 로 채워 오름차순 맨 앞에 섰다(useTableSort 와 같은 규칙). */
|
|
53
|
-
return
|
|
53
|
+
return compareCellBlankLast(va, vb, sortAsc);
|
|
54
54
|
});
|
|
55
55
|
}, [items, search, sortKey, sortAsc, searchEnabled, effectiveSearchFn]);
|
|
56
56
|
const totalPages = Math.max(1, Math.ceil(filtered.length / Number(pageSize)));
|
|
@@ -6,7 +6,7 @@ import Popup from './Popup';
|
|
|
6
6
|
import Button from '../Button';
|
|
7
7
|
import Dropdown from '../Dropdown';
|
|
8
8
|
import { getSavedTables, deleteTable as localDeleteTable } from '../../utils/savedWordTables';
|
|
9
|
-
import {
|
|
9
|
+
import { compareKorean } from '../../utils/textSort';
|
|
10
10
|
import { formatYmd } from '../../utils/date';
|
|
11
11
|
import './LoadTablePopup.css';
|
|
12
12
|
import { pressable } from '../../utils/pressable';
|
|
@@ -59,9 +59,9 @@ export default function LoadTablePopup({ onLoad, onCancel, tables: tablesProp, o
|
|
|
59
59
|
return copy.sort((a, b) => String(b.savedAt ?? '').localeCompare(String(a.savedAt ?? '')));
|
|
60
60
|
if (sortKey === 'oldest')
|
|
61
61
|
return copy.sort((a, b) => String(a.savedAt ?? '').localeCompare(String(b.savedAt ?? '')));
|
|
62
|
-
/* 이름은
|
|
62
|
+
/* 이름은 **`compareKorean`(콜레이터 하나)** — `localeCompare(…, 'ko')` 는 비교마다 콜레이터를 새로 만든다((113)) */
|
|
63
63
|
if (sortKey === 'name')
|
|
64
|
-
return copy.sort((a, b) =>
|
|
64
|
+
return copy.sort((a, b) => compareKorean(a.name, b.name));
|
|
65
65
|
if (sortKey === 'count')
|
|
66
66
|
return copy.sort((a, b) => b.items.length - a.items.length);
|
|
67
67
|
return copy;
|
|
@@ -19,21 +19,24 @@ export declare const NOISE_RATIO_STEP = 0.1;
|
|
|
19
19
|
export declare function noiseRatioOf(settings: Pick<CoreNoiseSettings, 'level' | 'mainVolume'> & {
|
|
20
20
|
noiseRatio?: number;
|
|
21
21
|
}): number;
|
|
22
|
-
|
|
22
|
+
/** `effectiveSpeechVolume`·`effectiveNoiseVolume`·`noiseElementVolume` 이 받는 것 —
|
|
23
|
+
* **공개 `d.ts` 에 이름이 그대로 나가는 자리**라 영어로 둔다. 안쪽 이름은 한글로 쓰는 저장소지만,
|
|
24
|
+
* 소비자가 편집기에서 보는 이름은 영어라야 한다(사용자 지시 2026-10-13). */
|
|
25
|
+
type SpeechVolumeSettings = Pick<CoreNoiseSettings, 'level' | 'mainVolume'> & {
|
|
23
26
|
noiseRatio?: number;
|
|
24
27
|
};
|
|
25
28
|
/** @public 문제 소리 볼륨(0~1) = mainVolume. 비율과 상관없다. */
|
|
26
|
-
export declare function effectiveSpeechVolume(settings:
|
|
29
|
+
export declare function effectiveSpeechVolume(settings: SpeechVolumeSettings): number;
|
|
27
30
|
/**
|
|
28
31
|
* @public 소음이 **들리는 크기**(0~2, 원본 소음 파일 기준) = mainVolume × 비율. 켜짐 여부는 보지
|
|
29
32
|
* 않는다. 결과 기록·파형이 쓴다. 소음 `<audio>` 에 거는 값은 `noiseElementVolume`.
|
|
30
33
|
*/
|
|
31
|
-
export declare function effectiveNoiseVolume(settings:
|
|
34
|
+
export declare function effectiveNoiseVolume(settings: SpeechVolumeSettings): number;
|
|
32
35
|
/**
|
|
33
36
|
* @public 소음 `<audio>` 볼륨(0~1) = min(1, 들리는 크기 ÷ gain). gain 은 소음 파일이 원본보다 몇 배
|
|
34
37
|
* 큰지 — 기본은 `configureStorage({ noiseFileGain })`(없으면 1).
|
|
35
38
|
*/
|
|
36
|
-
export declare function noiseElementVolume(settings:
|
|
39
|
+
export declare function noiseElementVolume(settings: SpeechVolumeSettings, gain?: number): number;
|
|
37
40
|
/**
|
|
38
41
|
* @public 설정에 조각을 덮는다 — `{ ...base, ...patch }` 에 비율 규칙 하나를 더한 것.
|
|
39
42
|
*
|
|
@@ -7,7 +7,7 @@ import Section from '../Section';
|
|
|
7
7
|
import './TrainingBlock.css';
|
|
8
8
|
import TrainingBlockButton from './TrainingBlockButton';
|
|
9
9
|
import { useWindowEvent } from '../../hooks/useWindowEvent';
|
|
10
|
-
import {
|
|
10
|
+
import { wasKeyboardPress } from '../../hooks/inputModality';
|
|
11
11
|
const BTN_COUNT_MAX = 20;
|
|
12
12
|
/** @public */
|
|
13
13
|
export default function TrainingBlock(props) {
|
|
@@ -35,7 +35,7 @@ export default function TrainingBlock(props) {
|
|
|
35
35
|
const 더보기초점Ref = useRef(null);
|
|
36
36
|
function loadMore() {
|
|
37
37
|
const 단추 = document.activeElement;
|
|
38
|
-
더보기초점Ref.current =
|
|
38
|
+
더보기초점Ref.current = wasKeyboardPress() && 단추 && 단추.closest?.('.training-block__more') ? visible : null;
|
|
39
39
|
setVisible((v) => Math.min(total, v + BTN_COUNT_MAX));
|
|
40
40
|
}
|
|
41
41
|
useEffect(() => {
|
|
@@ -39,7 +39,7 @@ const 움직임최소화 = () => {
|
|
|
39
39
|
};
|
|
40
40
|
/** 결과를 계산하는 중인 결과 창 자리 — 이 객체와 **같은 것**이면 계산 중이다(결과 창의 '완료' 를 막는다). */
|
|
41
41
|
const 결과계산중 = Object.freeze({});
|
|
42
|
-
import {
|
|
42
|
+
import { wasKeyboardPress } from '../../hooks/inputModality';
|
|
43
43
|
// 문장 4종은 각 PagePopup 래퍼에서 useTrainingSessionLog로 이미 기록 → 베이스에서 중복 방지
|
|
44
44
|
const SENTENCE_TIME_LOGGED_TYPES = new Set(['LISTEN_IN_NOISE', 'SHORT_STORY', 'LONG_STORY', 'ORDERING_SENTENCE']);
|
|
45
45
|
/** 결과 창 자료에서 정답/전체를 센다 — 결과 항목 형태가 훈련 타입마다 달라(소음 하 듣기/문장 순서화는 correct 대신
|
|
@@ -909,7 +909,7 @@ function TrainingPagePopupInner(props) {
|
|
|
909
909
|
if (앞 === undefined || 앞 === effectivePageIndex)
|
|
910
910
|
return;
|
|
911
911
|
// 키보드로 넘겼을 때만 — 마우스로 누른 사람에게 매 문제 '듣기' 초점 안내가 뜨지 않게
|
|
912
|
-
if (
|
|
912
|
+
if (!wasKeyboardPress())
|
|
913
913
|
return;
|
|
914
914
|
const panel = panelRef.current;
|
|
915
915
|
if (!panel)
|
|
@@ -19,7 +19,7 @@ import Button from '../Button';
|
|
|
19
19
|
import Icon from '../Icon';
|
|
20
20
|
import InfoTooltip from '../Tooltip/InfoTooltip';
|
|
21
21
|
import { STATUS_MESSAGES, ANNOUNCED_STATUSES } from './trainingStepAudioPlayerTypes';
|
|
22
|
-
import {
|
|
22
|
+
import { wasKeyboardPress } from '../../hooks/inputModality';
|
|
23
23
|
import { measureAudioDuration, waitForCanPlay } from './audioWaits';
|
|
24
24
|
import { useWaveReady } from './useWaveReady';
|
|
25
25
|
import { useObjectUrls } from './useObjectUrls';
|
|
@@ -1426,7 +1426,7 @@ export default function TrainingStepAudioPlayer({ btnLabel, soundSrc, bucketPath
|
|
|
1426
1426
|
누르면 소리를 받으러 가거나 "가져오지 못했습니다" 를 띄웠다 — 마우스와 같게 키보드도 막는다 */
|
|
1427
1427
|
disabled: isDisabled,
|
|
1428
1428
|
// 키보드로 눌렀을 때만 초점을 옮긴다 — 마우스로 누른 사람에게 '재생 / 멈춤' 초점 툴팁이 뜨지 않게
|
|
1429
|
-
onClick: () => { 듣기초점옮기기Ref.current =
|
|
1429
|
+
onClick: () => { 듣기초점옮기기Ref.current = wasKeyboardPress(); hideListenTip(); handleToggle(); }, "aria-label": typeof label === 'string' ? label : t('training.listen', '듣기'), onMouseEnter: showListenTip, onMouseLeave: hideListenTip,
|
|
1430
1430
|
// 키보드로 이 버튼에 오면 마우스와 동일하게 안내를 띄운다
|
|
1431
1431
|
onFocus: showListenTip, onBlur: hideListenTip, children: _jsx("span", { ref: listenBtnRef, className: "audio-player__button-triangle-wrap", children: _jsx("svg", { className: "audio-player__button-triangle", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { ref: triPathRef, d: "M13.2 7.7 L16.5 9.85 Q20.2 12 16.5 14.15 L10 18.3 Q7 19.7 7 16.3 L7 7.7 Q7 4.3 10 5.7 L13.2 7.7" }) }) }) })) : null, listenBtnRect && (_jsx(InfoTooltip, { anchorRect: listenBtnRect, text: typeof label === 'string' && label ? label : t('training.listenLabel', '문장 듣기'), offset: 8 })), _jsxs("div", { className: `audio-player__play ${stateLocal === "playMode" || displayedPlaying ? "is-visible" : ""}`, inert: !(stateLocal === "playMode" || displayedPlaying) || undefined, children: [_jsxs("div", { ref: btnsRef, className: "audio-player__buttons", children: [_jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "skip-back", onClick: handlePrev, ariaLabel: t('training.prev', '이전'), width: "60px", variant: "transparent-white", "aria-hidden": "true", tabIndex: -1, disabled: stateLocal !== 'playMode' }), _jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: displayedPlaying ? "pause" : "play", onClick: handleToggle, ariaLabel: t('common.playPause', '재생 / 멈춤'), "aria-pressed": displayedPlaying, width: "60px", variant: "transparent-white", disabled: stateLocal !== 'playMode' }), _jsx(Button, { htmlType: "button", size: 32, showTooltipInfo: true, type: "dot", shape: "pill", useBorder: false, icon: "square", onClick: handleStop, ariaLabel: t('training.stop', '정지'), width: "60px", variant: "transparent-white", "aria-hidden": "true", tabIndex: -1, disabled: stateLocal !== 'playMode' }), sourceButtonCount > 1 ? (_jsx("div", { ref: sourceButtonsRef, className: "audio-player__source-buttons", children: Array.from({ length: sourceButtonCount }).map((_, idx) => (_jsx(Button, { htmlType: "button", size: 32, type: "dot", shape: "pill", useBorder: false, label: String(idx + 1), onClick: () => { try {
|
|
1432
1432
|
handleJumpTo(idx);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
|
-
import {
|
|
5
|
+
import { wasKeyboardPress } from '../../hooks/inputModality';
|
|
6
6
|
import Button from '../Button';
|
|
7
7
|
import Toast from '../Toast';
|
|
8
8
|
import { useFontScale } from '../../hooks';
|
|
@@ -36,7 +36,7 @@ export default function TrainingStepTextBox({ context, state = "disabled", theme
|
|
|
36
36
|
const 펼친뒤초점Ref = useRef(false);
|
|
37
37
|
const handleActivate = useCallback(() => {
|
|
38
38
|
// 키보드로 눌렀을 때만 — 마우스로 눌렀는데 옮기면 '글자 크기 감소' 초점 툴팁이 떴다
|
|
39
|
-
펼친뒤초점Ref.current =
|
|
39
|
+
펼친뒤초점Ref.current = wasKeyboardPress() && !!상자Ref.current && 상자Ref.current.contains(document.activeElement);
|
|
40
40
|
setLocalStatus("active");
|
|
41
41
|
}, []);
|
|
42
42
|
const handleNoticeClose = useCallback(() => setNoticeMessage(''), []);
|
|
@@ -51,4 +51,4 @@ export interface OneShotSound {
|
|
|
51
51
|
* 풀린다. 부르는 쪽이 `try` 로 감싸지 않아도 되게 하려는 것이다. 넷 가운데 셋이 저마다
|
|
52
52
|
* `try` 를 적고 있었고, 그 안에서 하는 일이 조금씩 달랐다.
|
|
53
53
|
*/
|
|
54
|
-
export declare function playOneShot(url: string, onEnd?: (
|
|
54
|
+
export declare function playOneShot(url: string, onEnd?: (played: boolean) => void): OneShotSound;
|
|
@@ -41,15 +41,15 @@ export function playOneShot(url, onEnd) {
|
|
|
41
41
|
const done = new Promise((res) => { 풀기 = res; });
|
|
42
42
|
/** 끝은 한 번이다 — 거절과 `error` 가 함께 오는 일이 실제로 있다 */
|
|
43
43
|
let 끝났나 = false;
|
|
44
|
-
const 끝 = (
|
|
44
|
+
const 끝 = (played) => {
|
|
45
45
|
if (끝났나)
|
|
46
46
|
return;
|
|
47
47
|
끝났나 = true;
|
|
48
48
|
try {
|
|
49
|
-
onEnd?.(
|
|
49
|
+
onEnd?.(played);
|
|
50
50
|
}
|
|
51
51
|
catch { /* 부르는 쪽의 실패로 약속을 묶어 두지 않는다 */ }
|
|
52
|
-
풀기(
|
|
52
|
+
풀기(played);
|
|
53
53
|
};
|
|
54
54
|
let el;
|
|
55
55
|
try {
|
|
@@ -77,6 +77,6 @@
|
|
|
77
77
|
* > **고치는 방식**을 말하는지가 갈렸다. 그 세 번 동안 전해 받은 말로 짓지 않은 덕에
|
|
78
78
|
* > **되돌릴 것이 없었다.**
|
|
79
79
|
*/
|
|
80
|
-
export type
|
|
80
|
+
export type StepState = 'disabled' | 'done' | 'active';
|
|
81
81
|
/** 답을 고를 수 있나 — 닿지 않은 단계(`disabled`)만 막는다. @public */
|
|
82
|
-
export declare function canAnswerStep(state:
|
|
82
|
+
export declare function canAnswerStep(state: StepState | undefined): boolean;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* 비어 있어 **무엇을 틀렸는지 보여 줄 수가 없다.** 조용히 빼면 화면의 수가 전부인 줄
|
|
16
16
|
* 알게 되므로 몇 건을 뺐는지 함께 돌려준다.
|
|
17
17
|
*/
|
|
18
|
-
import {
|
|
18
|
+
import { compareKorean } from '../../utils/textSort';
|
|
19
19
|
/** 같은 시각 두 행의 차례 — 둘 다 id 가 있을 때만 가린다(숫자는 수 크기, 그 밖은 글자 사전순). 못 가리면 0. */
|
|
20
20
|
function 같은시각차례(a, b) {
|
|
21
21
|
if (a == null || b == null || a === '' || b === '')
|
|
@@ -99,7 +99,7 @@ export function summarizeMisses(records) {
|
|
|
99
99
|
lastSelected: v.lastSelected,
|
|
100
100
|
setIds: v.setIds,
|
|
101
101
|
}));
|
|
102
|
-
items.sort((a, b) => a.rate - b.rate || b.tried - a.tried ||
|
|
102
|
+
items.sort((a, b) => a.rate - b.rate || b.tried - a.tried || compareKorean(a.title, b.title));
|
|
103
103
|
return { items, counted, skippedNoTitle };
|
|
104
104
|
}
|
|
105
105
|
/**
|