@ihabdevteam/core 0.312.0 → 0.314.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 +78 -0
- package/dist/components.cjs.js +15 -10
- package/dist/hooks.cjs.js +3 -3
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +60 -55
- package/dist/ihabdevteam-core.d.ts +1 -1
- package/dist/packages/core/src/components/BarChart.js +4 -0
- package/dist/packages/core/src/components/Table.js +8 -0
- package/dist/packages/core/src/components/TextField.js +19 -3
- package/dist/packages/core/src/components/Word/wordTablePrint.js +5 -0
- package/dist/packages/core/src/components/WordTable.js +39 -7
- package/dist/packages/core/src/context/I18nProvider.js +11 -2
- package/dist/packages/core/src/hooks/useAnchoredMenu.js +19 -2
- package/dist/packages/core/src/utils/CategoryData.js +3 -1
- package/dist/packages/core/src/utils/categories.js +4 -1
- 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
|
@@ -1669,7 +1669,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1669
1669
|
*
|
|
1670
1670
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1671
1671
|
*/
|
|
1672
|
-
export declare const CORE_VERSION = "0.
|
|
1672
|
+
export declare const CORE_VERSION = "0.314.0";
|
|
1673
1673
|
|
|
1674
1674
|
/** @public */
|
|
1675
1675
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -129,6 +129,10 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, b
|
|
|
129
129
|
useEffect(() => {
|
|
130
130
|
if (!떠있음)
|
|
131
131
|
return;
|
|
132
|
+
/* **안-모음: 닫기만 한다**(코어 자체 조사 2026-10-10에서 세어 보고 적음).
|
|
133
|
+
굴림에 하는 일이 `setTooltip(null)` 하나뿐이라 레이아웃을 안 읽는다. 그리고 **미루면 안 된다** —
|
|
134
|
+
풍선은 막대에 붙어 떠 있어서, 한 틀 미루면 막대에서 떨어진 풍선이 그만큼 더 허공에 남는다
|
|
135
|
+
(Tooltip·useAnchoredMenu 의 `close` 길과 같은 가름: *자리 재기는 미뤄도 되고 닫기는 안 된다*). */
|
|
132
136
|
const 닫기 = () => setTooltip(null);
|
|
133
137
|
const 바깥누름 = (e) => {
|
|
134
138
|
const t = e.target;
|
|
@@ -227,6 +227,11 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
227
227
|
const 다음틀 = typeof requestAnimationFrame === 'function'
|
|
228
228
|
? requestAnimationFrame(() => 넘침재기())
|
|
229
229
|
: null;
|
|
230
|
+
/* **안-모음: 모을 값이 없다**(2026-10-10 조사에서 한 번 모아 봤다가 되돌림). `넘침재기` 는
|
|
231
|
+
`scrollWidth > clientWidth` 하나를 **불리언**으로 넣는데, 같은 값이면 React 가 그 자리에서
|
|
232
|
+
멈춘다 — 몰려 와도 다시 그리지 않는다. 그리고 바로 위 `requestAnimationFrame(() => 넘침재기())`
|
|
233
|
+
가 *자리잡은 뒤 한 번 더* 재는 일을 이미 하고, `test/tableScrollRegion.test.tsx` 가 **이 배선을
|
|
234
|
+
글자로 박아** 두었다(옛 결함을 지키는 못이다). 모으면 그 못이 지키던 모양이 바뀐다. */
|
|
230
235
|
window.addEventListener('resize', 넘침재기);
|
|
231
236
|
return () => {
|
|
232
237
|
if (다음틀 != null)
|
|
@@ -324,6 +329,9 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
324
329
|
: { index, startX: e.clientX, startW: measured[index] };
|
|
325
330
|
colWidthsRef.current = measured;
|
|
326
331
|
setColWidths(measured);
|
|
332
|
+
/* **안-모음: 곧바로 써야 한다** — `onResizeMove` 가 `colWidthsRef` 를 그 자리에서 맞춘다.
|
|
333
|
+
한 틀 미루면 마지막 이동 직후 손을 떼었을 때 **화면은 새 폭인데 저장소에는 한 박자 전 폭**이
|
|
334
|
+
남는다(그 함수 머리말에 적힌 까닭). 자리 재기와 달리 이것은 *값을 쓰는* 일이다. */
|
|
327
335
|
window.addEventListener('pointermove', onResizeMove);
|
|
328
336
|
window.addEventListener('pointerup', onResizeEnd);
|
|
329
337
|
// 터치 조절이 시스템 제스처 등으로 취소되면(pointercancel) 끝낸다 — 안 들으면 리스너가 남아 다음 스크롤 손짓이 폭을 바꿨다
|
|
@@ -72,17 +72,33 @@ export default function TextField(props) {
|
|
|
72
72
|
return;
|
|
73
73
|
목록자리재기();
|
|
74
74
|
/* 굴리거나 창 크기가 바뀌면 칸을 따라간다 — 목록 안을 굴리는 것은 무시 */
|
|
75
|
+
/* **틀에 한 번으로 모은다** — (115) 와 같은 꼴, 셋째 자리(2026-10-10, 코어 자체 조사). `목록자리재기` 는
|
|
76
|
+
`getBoundingClientRect()` 로 레이아웃을 읽고 상태를 바꾸는데, 이 듣는 이는 **문서 전체의 굴림을
|
|
77
|
+
capture 로** 받는다 — 목록이 뜬 동안 굴리면 틀마다 여러 번 돌았다. 여기는 **닫는 길이 없다**(자리만
|
|
78
|
+
따라간다) — 미뤄도 되는 일뿐이다. */
|
|
79
|
+
let 예약 = null;
|
|
80
|
+
const 자리다시 = () => {
|
|
81
|
+
if (typeof requestAnimationFrame === 'undefined') {
|
|
82
|
+
목록자리재기();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (예약 !== null)
|
|
86
|
+
return;
|
|
87
|
+
예약 = requestAnimationFrame(() => { 예약 = null; 목록자리재기(); });
|
|
88
|
+
};
|
|
75
89
|
const 굴림 = (e) => {
|
|
76
90
|
const t = e.target;
|
|
77
91
|
if (t && t.nodeType === 1 && t.closest?.('.text-field__suggestions'))
|
|
78
92
|
return;
|
|
79
|
-
|
|
93
|
+
자리다시();
|
|
80
94
|
};
|
|
81
95
|
window.addEventListener('scroll', 굴림, true);
|
|
82
|
-
window.addEventListener('resize',
|
|
96
|
+
window.addEventListener('resize', 자리다시);
|
|
83
97
|
return () => {
|
|
98
|
+
if (예약 !== null)
|
|
99
|
+
cancelAnimationFrame(예약);
|
|
84
100
|
window.removeEventListener('scroll', 굴림, true);
|
|
85
|
-
window.removeEventListener('resize',
|
|
101
|
+
window.removeEventListener('resize', 자리다시);
|
|
86
102
|
};
|
|
87
103
|
}, [목록떴나, 목록자리재기]);
|
|
88
104
|
// 외부 inputRef + 내부 autoRef 동시 연결 — 외부에서 전달받은 ref 객체(.current)에 노드를 채우는
|
|
@@ -197,6 +197,9 @@ export function captureHtml(els, paperW, paperH, autoPrint = false, fileName = '
|
|
|
197
197
|
});
|
|
198
198
|
}
|
|
199
199
|
scale();
|
|
200
|
+
/* 안-모음: 인쇄 창에 끼워 넣는 글이고, 아래 refit 과 함께 beforeprint 에서도 불린다 —
|
|
201
|
+
인쇄 배치를 뜨는 순간이라 미루면 **옛 배율로 인쇄된다**. resize 는 사람이 미리보기 창을
|
|
202
|
+
끄는 때뿐이라 잦지 않다. */
|
|
200
203
|
window.addEventListener('resize', scale);
|
|
201
204
|
|
|
202
205
|
/* **칸 글씨를 이 종이 칸에 맞춰 다시 센다.**
|
|
@@ -226,6 +229,8 @@ export function captureHtml(els, paperW, paperH, autoPrint = false, fileName = '
|
|
|
226
229
|
});
|
|
227
230
|
}
|
|
228
231
|
refit();
|
|
232
|
+
/* 안-모음: 바로 아래 beforeprint·print 매체질의가 **같은 함수**를 부른다 — 그 자리는 인쇄 배치가
|
|
233
|
+
잡히는 순간이라 못 미룬다(미루면 옛 배율로 인쇄된다, Letter 실측 -3.4% 가 그 꼴이었다). */
|
|
229
234
|
window.addEventListener('resize', refit);
|
|
230
235
|
/* 인쇄 배치로 바뀌는 순간을 브라우저마다 다른 신호로 알린다 — 크롬은 beforeprint,
|
|
231
236
|
파이어폭스는 print 매체 질의가 먼저 켜진다. 둘 다 걸어 두고 두 번 세도 값이 같다. */
|
|
@@ -317,22 +317,48 @@ export default function WordTable({ title, titleIcon, disc, className, orientati
|
|
|
317
317
|
거기서는 막을 수 없어, 익은 순간 직접 단다(passive:false). */
|
|
318
318
|
const 스크롤막기 = (ev) => { if (익음 && ev.cancelable)
|
|
319
319
|
ev.preventDefault(); };
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
320
|
+
/* **길이 둘이라 갈라 둔다**(2026-10-10, 코어 자체 조사). *문턱을 넘었나* 는 **안 미룬다** — 그 판정이
|
|
321
|
+
길게누름을 거두는 일이라, 한 틀 미루면 손가락을 움직였는데도 끌기가 익어 버린다. *밑에 어느 칸인가*
|
|
322
|
+
는 `elementFromPoint`(레이아웃을 읽는다) + 상태 바꾸기라 **틀에 한 번으로 모은다** — 보이는 일뿐이다. */
|
|
323
|
+
let 자리예약 = null;
|
|
324
|
+
let 마지막점 = null;
|
|
325
|
+
const 자리셈 = () => {
|
|
326
|
+
const pt = 마지막점;
|
|
327
|
+
if (!pt)
|
|
324
328
|
return;
|
|
325
|
-
}
|
|
326
329
|
/* 손가락 밑의 칸. `elementFromPoint` 가 없는 환경(구형·시험)에서는 자리를 못 고른다 —
|
|
327
330
|
그때는 떨어뜨려도 아무 일이 없다(끌기가 시작되지 않은 것과 같다). */
|
|
328
|
-
const 밑 = document.elementFromPoint?.(
|
|
331
|
+
const 밑 = document.elementFromPoint?.(pt.x, pt.y);
|
|
329
332
|
const 칸 = 밑?.closest?.('[data-wordtable-cell]');
|
|
330
333
|
const i = 칸 ? Number(칸.dataset.wordtableCell) : NaN;
|
|
331
334
|
const 자리 = Number.isInteger(i) ? i : null;
|
|
332
335
|
떨굴자리.current = 자리;
|
|
333
|
-
setDragOver(자리);
|
|
336
|
+
setDragOver(prev => (prev === 자리 ? prev : 자리));
|
|
337
|
+
};
|
|
338
|
+
const 이동 = (ev) => {
|
|
339
|
+
if (!익음) {
|
|
340
|
+
if (Math.abs(ev.clientX - 시작.x) > 움직임문턱_PX || Math.abs(ev.clientY - 시작.y) > 움직임문턱_PX)
|
|
341
|
+
해제();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
마지막점 = { x: ev.clientX, y: ev.clientY };
|
|
345
|
+
if (typeof requestAnimationFrame === 'undefined') {
|
|
346
|
+
자리셈();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (자리예약 !== null)
|
|
350
|
+
return;
|
|
351
|
+
자리예약 = requestAnimationFrame(() => { 자리예약 = null; 자리셈(); });
|
|
334
352
|
};
|
|
335
353
|
const 뗌 = () => {
|
|
354
|
+
/* **떼기 전에 밀린 셈을 치른다.** 자리셈을 틀로 모았으니, 움직이고 **같은 틀 안에** 떼면 틀이
|
|
355
|
+
오기 전에 여기 닿는다 — 그러면 `떨굴자리` 가 비어 **떨어뜨린 자리를 잃는다.** 옛 시험
|
|
356
|
+
(`wordTableReorder0916`)이 그것을 짚었다. 모으는 고침을 할 때는 **그 값을 읽는 쪽**도 봐야 한다. */
|
|
357
|
+
if (자리예약 !== null) {
|
|
358
|
+
cancelAnimationFrame(자리예약);
|
|
359
|
+
자리예약 = null;
|
|
360
|
+
자리셈();
|
|
361
|
+
}
|
|
336
362
|
const 자리 = 떨굴자리.current;
|
|
337
363
|
const 끌었다 = 익음;
|
|
338
364
|
해제();
|
|
@@ -344,6 +370,8 @@ export default function WordTable({ title, titleIcon, disc, className, orientati
|
|
|
344
370
|
set끌기(index);
|
|
345
371
|
떨굴자리.current = index;
|
|
346
372
|
setDragOver(index);
|
|
373
|
+
/* **안-모음: `preventDefault()` 는 못 미룬다** — 틀로 미루면 이벤트가 이미 지나가 막을 수 없다
|
|
374
|
+
(`passive: false` 로 다는 까닭이 그것이다). */
|
|
347
375
|
window.addEventListener('touchmove', 스크롤막기, { passive: false });
|
|
348
376
|
}, 길게누름_MS);
|
|
349
377
|
function 해제() {
|
|
@@ -353,6 +381,10 @@ export default function WordTable({ title, titleIcon, disc, className, orientati
|
|
|
353
381
|
window.removeEventListener('pointercancel', 뗌);
|
|
354
382
|
window.removeEventListener('touchmove', 스크롤막기);
|
|
355
383
|
익음 = false;
|
|
384
|
+
if (자리예약 !== null) {
|
|
385
|
+
cancelAnimationFrame(자리예약);
|
|
386
|
+
자리예약 = null;
|
|
387
|
+
}
|
|
356
388
|
떨굴자리.current = null;
|
|
357
389
|
풀기.current = null;
|
|
358
390
|
set끌기(null);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect } from 'react';
|
|
3
|
+
import { useEffect, useMemo } from 'react';
|
|
4
4
|
import i18n, { SUPPORTED_LOCALES, DEFAULT_LOCALE } from '../i18n';
|
|
5
5
|
import { I18nContext } from './I18nContext';
|
|
6
6
|
function 지원코드(locale) {
|
|
@@ -20,5 +20,14 @@ export function I18nProvider({ locale = DEFAULT_LOCALE, children }) {
|
|
|
20
20
|
i18n.changeLanguage(code);
|
|
21
21
|
}
|
|
22
22
|
}, [code]);
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* **값을 기억해 둔다** — (114) 와 **같은 꼴, 둘째 자리**다(2026-10-10, 코어 자체 조사).
|
|
25
|
+
* (114) 로 `WordDataProvider` 를 고칠 때 **프로바이더를 세지 않았다** — 리터럴을 바로 넘기는 자리가
|
|
26
|
+
* 둘이었고 하나만 고쳤다. 이 프로바이더도 앱 전체를 감싸고, `useI18n()` 은 공개 훅이라 쓰는 쪽을 코어가 모른다.
|
|
27
|
+
*
|
|
28
|
+
* `code` 는 글자이고 `SUPPORTED_LOCALES` 는 모듈 상수라 **값은 늘 같은데 객체만 새로** 났다 —
|
|
29
|
+
* 가장 안 보이는 꼴이다(값이 틀리지 않으니 아무도 안 묻는다).
|
|
30
|
+
*/
|
|
31
|
+
const value = useMemo(() => ({ locale: code, supportedLocales: SUPPORTED_LOCALES }), [code]);
|
|
32
|
+
return (_jsx(I18nContext.Provider, { value: value, children: children }));
|
|
24
33
|
}
|
|
@@ -89,11 +89,26 @@ export function useAnchoredMenu({ computeStyle, onScroll = 'close', onResize = '
|
|
|
89
89
|
document.addEventListener('mousedown', handleOutsideClick);
|
|
90
90
|
// iOS 는 누를 수 없는 요소를 톡하면 mousedown 을 안 보낸다 — pointerdown 으로도 닫는다(Tooltip·CategoryIcon 과 같게)
|
|
91
91
|
document.addEventListener('pointerdown', handleOutsideClick);
|
|
92
|
+
/* **자리 다시 재기는 틀에 한 번으로 모은다** — (115) 와 같은 꼴, 둘째 자리(2026-10-10, 코어 자체 조사).
|
|
93
|
+
`recalc()` 은 `getBoundingClientRect()` 로 레이아웃을 읽는데, 이 듣는 이는 **문서 전체의 굴림을
|
|
94
|
+
capture 로** 받는다 — `DatePicker` 가 `onScroll: 'reposition'` 을 주므로 달력이 열린 동안 굴리면
|
|
95
|
+
틀마다 여러 번 돌았다. **닫는 길('close')은 안 미룬다** — 미루면 앵커에서 떨어진 판이 한 틀 더
|
|
96
|
+
허공에 남는다(Tooltip 과 같은 가름). 미루는 것은 *자리* 뿐이다. */
|
|
97
|
+
let 자리예약 = null;
|
|
98
|
+
const 자리다시 = () => {
|
|
99
|
+
if (typeof requestAnimationFrame === 'undefined') {
|
|
100
|
+
recalc();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (자리예약 !== null)
|
|
104
|
+
return;
|
|
105
|
+
자리예약 = requestAnimationFrame(() => { 자리예약 = null; recalc(); });
|
|
106
|
+
};
|
|
92
107
|
const handleScroll = (e) => {
|
|
93
108
|
if (menuRef.current?.contains(e.target))
|
|
94
109
|
return;
|
|
95
110
|
if (onScroll === 'reposition')
|
|
96
|
-
|
|
111
|
+
자리다시();
|
|
97
112
|
else if (onScroll === 'close')
|
|
98
113
|
setOpen(false);
|
|
99
114
|
};
|
|
@@ -101,7 +116,7 @@ export function useAnchoredMenu({ computeStyle, onScroll = 'close', onResize = '
|
|
|
101
116
|
window.addEventListener('scroll', handleScroll, true);
|
|
102
117
|
const handleResize = () => {
|
|
103
118
|
if (onResize === 'reposition')
|
|
104
|
-
|
|
119
|
+
자리다시();
|
|
105
120
|
else if (onResize === 'close')
|
|
106
121
|
setOpen(false);
|
|
107
122
|
};
|
|
@@ -119,6 +134,8 @@ export function useAnchoredMenu({ computeStyle, onScroll = 'close', onResize = '
|
|
|
119
134
|
window.addEventListener('orientationchange', handleOrientation);
|
|
120
135
|
}
|
|
121
136
|
return () => {
|
|
137
|
+
if (자리예약 !== null)
|
|
138
|
+
cancelAnimationFrame(자리예약);
|
|
122
139
|
판관찰?.disconnect();
|
|
123
140
|
document.removeEventListener('mousedown', handleOutsideClick);
|
|
124
141
|
document.removeEventListener('pointerdown', handleOutsideClick);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** @public */
|
|
2
2
|
export const TRAINING_TYPES = [
|
|
3
|
-
|
|
3
|
+
/* 아이콘은 **`categories.ts` 의 `DEFAULT_TRAINING_TYPES` 와 같아야 한다** — 말귀는 그쪽 하나만 짚었는데
|
|
4
|
+
세어 보니 **둘**이었고, 화면에 나가는 것은 이쪽(`translateTrainingTypes`)이다. (116), 2026-10-10. */
|
|
5
|
+
{ key: "LISTEN_IN_NOISE", label: "소음 하 문장 듣기", icon: "audio-lines", desc: "다양한 소음 환경에서 문장 청력을 훈련합니다." },
|
|
4
6
|
{ key: "SHORT_STORY", label: "짧은 이야기 듣기", icon: "message-square", desc: "짧은 이야기를 듣고 기억하여 문제를 풀어봅니다." },
|
|
5
7
|
{ key: "LONG_STORY", label: "긴 이야기 듣기", icon: "message-square-text", desc: "긴 이야기를 듣고 기억하여 다수의 문제를 풀어봅니다." },
|
|
6
8
|
{ key: "ORDERING_SENTENCE", label: "문장 순서화 하기", icon: "align-start-vertical", desc: "여러개의 짧은 문장을 듣고 기억하여 그 순서에 맞게 배열합니다." },
|
|
@@ -33,7 +33,10 @@ const DEFAULT_TOPIC_TYPES = [
|
|
|
33
33
|
{ key: 'SPRT', label: '스포츠', icon: 'trophy', color: '#FFCA28', subs: [{ key: 'INVDSP', label: '개인경기' }, { key: 'MARART', label: '격투기' }, { key: 'TEAMSP', label: '단체경기' }, { key: 'FOOTBL', label: '축구' }, { key: 'GESPKL', label: '스포츠 상식' }] },
|
|
34
34
|
];
|
|
35
35
|
const DEFAULT_TRAINING_TYPES = [
|
|
36
|
-
|
|
36
|
+
/* 아이콘은 **`CategoryData.ts` 의 `TRAINING_TYPES` 와 같아야 한다** — 같은 결정이 두 목록에 적혀 있다
|
|
37
|
+
(못: `test/trainingTypeIconsAgree.test.ts`). (116) 에서 `party-popper`(🎉) 를 걷었다: 자리를 채워 둔
|
|
38
|
+
값이 두 앱 숙제 카드에 그대로 나갔고, 네 갈래 중 **가장 많이 쓰이는** 갈래라 목록이 폭죽밭이었다. */
|
|
39
|
+
{ key: 'LISTEN_IN_NOISE', label: '소음 하 문장 듣기', icon: 'audio-lines', desc: '소음 환경 속 문장 이해 훈련' },
|
|
37
40
|
{ key: 'SHORT_STORY', label: '짧은 이야기 듣기', icon: 'message-square', desc: '짧은 이야기 + 4문제' },
|
|
38
41
|
{ key: 'LONG_STORY', label: '긴 이야기 듣기', icon: 'message-square-text', desc: '긴 이야기 심화 훈련' },
|
|
39
42
|
{ key: 'ORDERING_SENTENCE', label: '문장 순서화 하기', icon: 'align-start-vertical', desc: '순서 배열 훈련' },
|