@ihabdevteam/core 0.192.0 → 0.194.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.
@@ -1430,7 +1430,16 @@ export declare interface ConfirmPopupProps {
1430
1430
  title: ReactNode;
1431
1431
  /** 본문 메시지 */
1432
1432
  message: ReactNode;
1433
- /** details/message 위(본문 최상단)에 놓을 콘텐츠 — 미리보기 카드 등. 가운데 정렬. */
1433
+ /**
1434
+ * details·message 위(본문 최상단)에 놓을 것 — 미리보기 카드·표 등.
1435
+ *
1436
+ * **여럿을 주면 세로로 쌓인다**(가로는 가운데, 사이는 `--spacing-m`). 예전에는 가로 줄이라
1437
+ * 둘을 넣으면 나란히 섰고, 그래서 부르는 쪽마다 세로 통을 하나씩 더 두르고 있었다(0.193.0 에
1438
+ * 여기로). 제 폭을 정한 것(`width: 100%` 인 표 틀·입력칸)은 그대로 꽉 찬다.
1439
+ *
1440
+ * 이 자리를 주면 **확인창의 더해 둔 위 여백도 되돌린다** — 글만 있는 확인창에서는 그 한 뼘이
1441
+ * 숨 쉴 자리지만, 맨 위에 표나 카드가 서면 제목과 내용을 공연히 벌려 놓는다.
1442
+ */
1434
1443
  topContent?: ReactNode;
1435
1444
  /**
1436
1445
  * 결정에 필요한 정보를 표(header 없는 Panel)로 message 위에 보여줄 때 사용.
@@ -1617,7 +1626,7 @@ export declare function copyText(text: string): Promise<boolean>;
1617
1626
  *
1618
1627
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
1619
1628
  */
1620
- export declare const CORE_VERSION = "0.192.0";
1629
+ export declare const CORE_VERSION = "0.194.0";
1621
1630
 
1622
1631
  /** @public */
1623
1632
  declare const coreDefault: Record<string, unknown>;
@@ -4880,6 +4889,35 @@ export declare interface PopupProps {
4880
4889
 
4881
4890
  export declare const POS_LABEL: Record<string, string>;
4882
4891
 
4892
+ /**
4893
+ * **늦게 받는 화면 청크를 한가할 때 미리 받아 둔다.**
4894
+ *
4895
+ * `lazyPage` 로 미룬 화면은 **그리려는 순간** 청크를 받는다. 그 사이 화면에는 동그라미만 돌고
4896
+ * 제목도 구획도 하나도 없다(말귀 실측 2026-09-20: 고객 상세가 누른 뒤 **364ms** 동안 빈 화면.
4897
+ * 사용자가 짚었다 — "고객을 누르면 pagetitle, 차트섹션들이 하나도 안 보이는 순간이 있는데").
4898
+ *
4899
+ * 그래서 **누르기 전에** 받아 둔다. 목록이 선 뒤 한가할 때 같은 모듈을 한 번 부르면 번들러가
4900
+ * 캐시하고, 누르는 순간 `lazy` 가 곧바로 풀려 **뼈대가 먼저 선다** — 자료를 기다리는 일은 그
4901
+ * 뼈대 안에서 일어난다.
4902
+ *
4903
+ * 지키는 것 둘:
4904
+ * - **첫 그리기를 안 건드린다.** 화면이 선 뒤 한가한 틈에만 받는다(`requestIdleCallback`).
4905
+ * 그 함수는 **사파리에 없다** — 없으면 짧게 미뤄(300ms) 그리는 일을 막지 않을 만큼만 늦춘다.
4906
+ * - **실패를 삼킨다.** 미리 받기가 안 되면 누를 때 받으면 된다(그러면 예전과 같을 뿐이다).
4907
+ * 그것 때문에 오류 화면을 띄울 일이 아니다.
4908
+ *
4909
+ * 돌려주는 것은 **그만두는 함수**다 — 효과에서 그대로 돌려주면 화면을 떠날 때 예약이 걷힌다.
4910
+ *
4911
+ * ```ts
4912
+ * useEffect(() => prefetchChunk(() => import('./pages/CustomerDetail')), [])
4913
+ * ```
4914
+ *
4915
+ * 두 앱(회원앱 · 어드민)이 한 벌씩 들고 있던 것이다 — 번들이 갈려 앱끼리는 합칠 수 없는
4916
+ * 자리라 코어로 왔다(2026-09-25). 브라우저 차이를 아는 일은 `chunkSkew`·`lazyPage` 와 같은 결이다.
4917
+ * @public
4918
+ */
4919
+ export declare function prefetchChunk(...loads: readonly (() => Promise<unknown>)[]): () => void;
4920
+
4883
4921
  /**
4884
4922
  * @public
4885
4923
  * 여러 단어의 이미지 존재 여부를 사전 확인 (병렬). localStorage에 영구 저장됨.
@@ -9029,6 +9067,29 @@ export declare function usePagedList<T>(items: T[], perPage: number, page: numbe
9029
9067
  curPage: number;
9030
9068
  };
9031
9069
 
9070
+ /**
9071
+ * **거르는 조건이 바뀌면 첫 쪽으로 돌아간다.** `usePagedList` 의 짝이다.
9072
+ *
9073
+ * 안 돌리면 **3쪽을 보던 채로 검색어를 바꿨을 때 결과가 한 쪽뿐이라 빈 표가 뜬다** — 찾는 것이
9074
+ * 없어서가 아니라 **없는 쪽을 보고 있어서**다. 화면 셋(어드민 문장·낱말 자료, 회원앱 낱말 관리)이
9075
+ * 같은 세 줄을 각자 적고 있었고, 그것을 한 벌로 모은 뒤에도 두 앱에 한 벌씩 남아 있었다
9076
+ * (번들이 갈려 앱끼리는 합칠 수 없는 자리다 — 2026-09-25 코어로).
9077
+ *
9078
+ * **효과(`useEffect`)가 아니라 렌더 중에 고친다.** 효과로 미루면 한 번은 옛 쪽 번호로 그려진 뒤
9079
+ * 다음 프레임에 1쪽이 되어, 새 조건의 결과가 **빈 표로 한 번 깜빡인다.** 렌더 중 상태 조정은
9080
+ * React 가 이 꼴을 위해 내준 길이다(같은 컴포넌트를 바로 다시 그린다).
9081
+ *
9082
+ * ```ts
9083
+ * usePageResetOnChange(`${검색어}|${갈래}|${정렬}`, setPage)
9084
+ * const { pageItems, totalPages } = usePagedList(걸러낸것, 쪽크기, page)
9085
+ * ```
9086
+ *
9087
+ * @param 열쇠 조건을 하나로 이어 붙인 글 — 이것이 달라지면 첫 쪽이다
9088
+ * @param setPage 쪽 번호를 쥔 쪽에서 준다
9089
+ * @public
9090
+ */
9091
+ export declare function usePageResetOnChange(열쇠: string, setPage: (n: number) => void): void;
9092
+
9032
9093
  /**
9033
9094
  * 비밀번호 변경 폼 상태와 핸들러를 캡슐화한 훅.
9034
9095
  * showModal 상태는 컴포넌트가 직접 관리 (훅 밖에 두어야 모달 열기 트리거 제어 가능).
@@ -6,7 +6,16 @@ export interface ConfirmPopupProps {
6
6
  title: ReactNode;
7
7
  /** 본문 메시지 */
8
8
  message: ReactNode;
9
- /** details/message 위(본문 최상단)에 놓을 콘텐츠 — 미리보기 카드 등. 가운데 정렬. */
9
+ /**
10
+ * details·message 위(본문 최상단)에 놓을 것 — 미리보기 카드·표 등.
11
+ *
12
+ * **여럿을 주면 세로로 쌓인다**(가로는 가운데, 사이는 `--spacing-m`). 예전에는 가로 줄이라
13
+ * 둘을 넣으면 나란히 섰고, 그래서 부르는 쪽마다 세로 통을 하나씩 더 두르고 있었다(0.193.0 에
14
+ * 여기로). 제 폭을 정한 것(`width: 100%` 인 표 틀·입력칸)은 그대로 꽉 찬다.
15
+ *
16
+ * 이 자리를 주면 **확인창의 더해 둔 위 여백도 되돌린다** — 글만 있는 확인창에서는 그 한 뼘이
17
+ * 숨 쉴 자리지만, 맨 위에 표나 카드가 서면 제목과 내용을 공연히 벌려 놓는다.
18
+ */
10
19
  topContent?: ReactNode;
11
20
  /**
12
21
  * 결정에 필요한 정보를 표(header 없는 Panel)로 message 위에 보여줄 때 사용.
@@ -74,7 +74,9 @@ export default function ConfirmPopup(props) {
74
74
  // onConfirm(서버 요청 등)이 진행 중일 때 X/ESC로 닫으면, 팝업은 즉시 사라지는데
75
75
  // 이미 시작된 요청은 계속 진행돼 완료 후 부모 state/토스트가 뒤늦게 갱신된다 —
76
76
  // 사용자는 "취소했다"고 믿었는데 실제로는 실행된 것처럼 보이는 문제를 막는다.
77
- closeDisabled: busy, inline: inline, size: size, className: cx('confirm-popup', className), buttons: [
77
+ closeDisabled: busy, inline: inline, size: size,
78
+ /* 위에 미리보기가 서면 더해 둔 위 여백을 되돌린다 — 까닭은 `Popup.css` 의 그 규칙에 */
79
+ className: cx('confirm-popup', topContent != null && 'confirm-popup--has-top', className), buttons: [
78
80
  ...(extraAction ? [{
79
81
  label: extraAction.label,
80
82
  variant: extraAction.variant ?? 'default',
@@ -202,7 +202,25 @@
202
202
  /* 확인 팝업은 본문 위/아래 여백을 20px씩 더(기본 20 → 40) */
203
203
  .confirm-popup .popup-content { padding-top: 40px; }
204
204
  .confirm-popup .popup-content__inner { padding-bottom: 40px; }
205
+ /* **위에 미리보기가 서면 그 더한 여백은 되돌린다.** 글만 있는 확인창에서는 그 한 뼘이 숨 쉴
206
+ 자리지만, 맨 위에 표나 카드가 서면 제목과 내용을 공연히 벌려 놓는다(사용자 지시 2026-09-23
207
+ "여백이 많아보여"). 부르는 쪽 둘이 각자 되돌리고 있던 것을 여기서 한다(2026-09-25). */
208
+ .confirm-popup--has-top .popup-content { padding-top: var(--spacing-l); }
205
209
  /* details Panel과 아래 메시지 사이 간격 */
206
210
  .confirm-popup .panel + .confirm-popup__message { margin-top: 16px; }
207
- /* 최상단 미리보기(카드 등) — 가운데 정렬 + 아래 여백 */
208
- .confirm-popup__top { display: flex; justify-content: center; margin-bottom: 20px; }
211
+ /* **최상단 자리 — 세로로 쌓고 가로는 가운데.**
212
+ 예전에는 가로 줄(`justify-content: center`)이라, 이 자리에 둘을 넣으면 **나란히 섰다.**
213
+ 그래서 둘 이상을 세워야 하는 창마다 세로 통을 하나씩 더 두르고 있었다(말귀 두 벌 — 표 저장
214
+ 확인창·숙제 확인창. 두 파일이 "가로 가운데라 그냥 넣으면 나란히 선다" 는 말까지 따로 적어
215
+ 두었다 — 그 말이 두 군데 있다는 것 자체가 여기 일이라는 신호였다, 2026-09-25).
216
+
217
+ 하나만 넣던 자리는 그대로다: 가운데 맞춤(`align-items: center`)은 **제 폭을 안 정한 것**에만
218
+ 걸리므로, `width: 100%` 를 가진 것(표 틀·입력칸)은 여전히 꽉 찬다. */
219
+ .confirm-popup__top {
220
+ display: flex;
221
+ flex-direction: column;
222
+ align-items: center;
223
+ gap: var(--spacing-m);
224
+ margin-bottom: 20px;
225
+ }
226
+ .confirm-popup__top > * { max-width: 100%; }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * **거르는 조건이 바뀌면 첫 쪽으로 돌아간다.** `usePagedList` 의 짝이다.
3
+ *
4
+ * 안 돌리면 **3쪽을 보던 채로 검색어를 바꿨을 때 결과가 한 쪽뿐이라 빈 표가 뜬다** — 찾는 것이
5
+ * 없어서가 아니라 **없는 쪽을 보고 있어서**다. 화면 셋(어드민 문장·낱말 자료, 회원앱 낱말 관리)이
6
+ * 같은 세 줄을 각자 적고 있었고, 그것을 한 벌로 모은 뒤에도 두 앱에 한 벌씩 남아 있었다
7
+ * (번들이 갈려 앱끼리는 합칠 수 없는 자리다 — 2026-09-25 코어로).
8
+ *
9
+ * **효과(`useEffect`)가 아니라 렌더 중에 고친다.** 효과로 미루면 한 번은 옛 쪽 번호로 그려진 뒤
10
+ * 다음 프레임에 1쪽이 되어, 새 조건의 결과가 **빈 표로 한 번 깜빡인다.** 렌더 중 상태 조정은
11
+ * React 가 이 꼴을 위해 내준 길이다(같은 컴포넌트를 바로 다시 그린다).
12
+ *
13
+ * ```ts
14
+ * usePageResetOnChange(`${검색어}|${갈래}|${정렬}`, setPage)
15
+ * const { pageItems, totalPages } = usePagedList(걸러낸것, 쪽크기, page)
16
+ * ```
17
+ *
18
+ * @param 열쇠 조건을 하나로 이어 붙인 글 — 이것이 달라지면 첫 쪽이다
19
+ * @param setPage 쪽 번호를 쥔 쪽에서 준다
20
+ * @public
21
+ */
22
+ export declare function usePageResetOnChange(열쇠: string, setPage: (n: number) => void): void;
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+ import { useState } from 'react';
3
+ /**
4
+ * **거르는 조건이 바뀌면 첫 쪽으로 돌아간다.** `usePagedList` 의 짝이다.
5
+ *
6
+ * 안 돌리면 **3쪽을 보던 채로 검색어를 바꿨을 때 결과가 한 쪽뿐이라 빈 표가 뜬다** — 찾는 것이
7
+ * 없어서가 아니라 **없는 쪽을 보고 있어서**다. 화면 셋(어드민 문장·낱말 자료, 회원앱 낱말 관리)이
8
+ * 같은 세 줄을 각자 적고 있었고, 그것을 한 벌로 모은 뒤에도 두 앱에 한 벌씩 남아 있었다
9
+ * (번들이 갈려 앱끼리는 합칠 수 없는 자리다 — 2026-09-25 코어로).
10
+ *
11
+ * **효과(`useEffect`)가 아니라 렌더 중에 고친다.** 효과로 미루면 한 번은 옛 쪽 번호로 그려진 뒤
12
+ * 다음 프레임에 1쪽이 되어, 새 조건의 결과가 **빈 표로 한 번 깜빡인다.** 렌더 중 상태 조정은
13
+ * React 가 이 꼴을 위해 내준 길이다(같은 컴포넌트를 바로 다시 그린다).
14
+ *
15
+ * ```ts
16
+ * usePageResetOnChange(`${검색어}|${갈래}|${정렬}`, setPage)
17
+ * const { pageItems, totalPages } = usePagedList(걸러낸것, 쪽크기, page)
18
+ * ```
19
+ *
20
+ * @param 열쇠 조건을 하나로 이어 붙인 글 — 이것이 달라지면 첫 쪽이다
21
+ * @param setPage 쪽 번호를 쥔 쪽에서 준다
22
+ * @public
23
+ */
24
+ export function usePageResetOnChange(열쇠, setPage) {
25
+ const [본열쇠, set본열쇠] = useState(열쇠);
26
+ if (본열쇠 !== 열쇠) {
27
+ set본열쇠(열쇠);
28
+ setPage(1);
29
+ }
30
+ }
@@ -23,6 +23,7 @@ export * from './hooks/useEscToClose';
23
23
  export * from './hooks/useWordMeanings';
24
24
  export * from './hooks/useHoverAnchor';
25
25
  export * from './hooks/usePagedList';
26
+ export * from './hooks/usePageResetOnChange';
26
27
  export * from './hooks/useToastState';
27
28
  export { useTrainingTypeLabels } from './hooks/useTrainingTypeLabels';
28
29
  export { CORE_VERSION } from './version';
@@ -22,6 +22,7 @@ export * from './hooks/useEscToClose';
22
22
  export * from './hooks/useWordMeanings';
23
23
  export * from './hooks/useHoverAnchor';
24
24
  export * from './hooks/usePagedList';
25
+ export * from './hooks/usePageResetOnChange';
25
26
  // 토스트 상태(문구 + 종류 + 재실행 번호). 회원 앱의 useToast 가 이 위에 얹혀 있다 —
26
27
  // 같은 관용구를 두 벌로 두지 않으려면 패키지 밖에서도 보여야 한다.
27
28
  export * from './hooks/useToastState';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * **늦게 받는 화면 청크를 한가할 때 미리 받아 둔다.**
3
+ *
4
+ * `lazyPage` 로 미룬 화면은 **그리려는 순간** 청크를 받는다. 그 사이 화면에는 동그라미만 돌고
5
+ * 제목도 구획도 하나도 없다(말귀 실측 2026-09-20: 고객 상세가 누른 뒤 **364ms** 동안 빈 화면.
6
+ * 사용자가 짚었다 — "고객을 누르면 pagetitle, 차트섹션들이 하나도 안 보이는 순간이 있는데").
7
+ *
8
+ * 그래서 **누르기 전에** 받아 둔다. 목록이 선 뒤 한가할 때 같은 모듈을 한 번 부르면 번들러가
9
+ * 캐시하고, 누르는 순간 `lazy` 가 곧바로 풀려 **뼈대가 먼저 선다** — 자료를 기다리는 일은 그
10
+ * 뼈대 안에서 일어난다.
11
+ *
12
+ * 지키는 것 둘:
13
+ * - **첫 그리기를 안 건드린다.** 화면이 선 뒤 한가한 틈에만 받는다(`requestIdleCallback`).
14
+ * 그 함수는 **사파리에 없다** — 없으면 짧게 미뤄(300ms) 그리는 일을 막지 않을 만큼만 늦춘다.
15
+ * - **실패를 삼킨다.** 미리 받기가 안 되면 누를 때 받으면 된다(그러면 예전과 같을 뿐이다).
16
+ * 그것 때문에 오류 화면을 띄울 일이 아니다.
17
+ *
18
+ * 돌려주는 것은 **그만두는 함수**다 — 효과에서 그대로 돌려주면 화면을 떠날 때 예약이 걷힌다.
19
+ *
20
+ * ```ts
21
+ * useEffect(() => prefetchChunk(() => import('./pages/CustomerDetail')), [])
22
+ * ```
23
+ *
24
+ * 두 앱(회원앱 · 어드민)이 한 벌씩 들고 있던 것이다 — 번들이 갈려 앱끼리는 합칠 수 없는
25
+ * 자리라 코어로 왔다(2026-09-25). 브라우저 차이를 아는 일은 `chunkSkew`·`lazyPage` 와 같은 결이다.
26
+ * @public
27
+ */
28
+ export declare function prefetchChunk(...loads: readonly (() => Promise<unknown>)[]): () => void;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * **늦게 받는 화면 청크를 한가할 때 미리 받아 둔다.**
3
+ *
4
+ * `lazyPage` 로 미룬 화면은 **그리려는 순간** 청크를 받는다. 그 사이 화면에는 동그라미만 돌고
5
+ * 제목도 구획도 하나도 없다(말귀 실측 2026-09-20: 고객 상세가 누른 뒤 **364ms** 동안 빈 화면.
6
+ * 사용자가 짚었다 — "고객을 누르면 pagetitle, 차트섹션들이 하나도 안 보이는 순간이 있는데").
7
+ *
8
+ * 그래서 **누르기 전에** 받아 둔다. 목록이 선 뒤 한가할 때 같은 모듈을 한 번 부르면 번들러가
9
+ * 캐시하고, 누르는 순간 `lazy` 가 곧바로 풀려 **뼈대가 먼저 선다** — 자료를 기다리는 일은 그
10
+ * 뼈대 안에서 일어난다.
11
+ *
12
+ * 지키는 것 둘:
13
+ * - **첫 그리기를 안 건드린다.** 화면이 선 뒤 한가한 틈에만 받는다(`requestIdleCallback`).
14
+ * 그 함수는 **사파리에 없다** — 없으면 짧게 미뤄(300ms) 그리는 일을 막지 않을 만큼만 늦춘다.
15
+ * - **실패를 삼킨다.** 미리 받기가 안 되면 누를 때 받으면 된다(그러면 예전과 같을 뿐이다).
16
+ * 그것 때문에 오류 화면을 띄울 일이 아니다.
17
+ *
18
+ * 돌려주는 것은 **그만두는 함수**다 — 효과에서 그대로 돌려주면 화면을 떠날 때 예약이 걷힌다.
19
+ *
20
+ * ```ts
21
+ * useEffect(() => prefetchChunk(() => import('./pages/CustomerDetail')), [])
22
+ * ```
23
+ *
24
+ * 두 앱(회원앱 · 어드민)이 한 벌씩 들고 있던 것이다 — 번들이 갈려 앱끼리는 합칠 수 없는
25
+ * 자리라 코어로 왔다(2026-09-25). 브라우저 차이를 아는 일은 `chunkSkew`·`lazyPage` 와 같은 결이다.
26
+ * @public
27
+ */
28
+ export function prefetchChunk(...loads) {
29
+ let 취소됨 = false;
30
+ const 받기 = () => { if (!취소됨)
31
+ for (const load of loads)
32
+ void load().catch(() => { }); };
33
+ const idle = globalThis.requestIdleCallback;
34
+ if (idle) {
35
+ const id = idle(받기, { timeout: 2000 });
36
+ return () => {
37
+ 취소됨 = true;
38
+ const cancel = globalThis.cancelIdleCallback;
39
+ cancel?.(id);
40
+ };
41
+ }
42
+ const id = setTimeout(받기, 300);
43
+ return () => { 취소됨 = true; clearTimeout(id); };
44
+ }
@@ -57,4 +57,5 @@ export { PLAN_FEATURES, PLAN_LIMITS, TIER_RANK, getPlanTier, isManagerTier, type
57
57
  export { WORD_SUGGESTION_WEEKLY_QUOTA, WORD_SUGGESTION_ACCOUNT_LIMIT, WORD_SUGGESTION_EXISTING_ERROR, isThisWeekPick, } from './utils/wordSuggestionPolicy';
58
58
  export { recoverFromSkew, skewReloadPending } from './utils/chunkSkew';
59
59
  export { lazyPage } from './utils/lazyPage';
60
+ export { prefetchChunk } from './utils/prefetchChunk';
60
61
  export { CORE_VERSION } from './version';
@@ -46,4 +46,5 @@ export { PLAN_FEATURES, PLAN_LIMITS, TIER_RANK, getPlanTier, isManagerTier, } fr
46
46
  export { WORD_SUGGESTION_WEEKLY_QUOTA, WORD_SUGGESTION_ACCOUNT_LIMIT, WORD_SUGGESTION_EXISTING_ERROR, isThisWeekPick, } from './utils/wordSuggestionPolicy';
47
47
  export { recoverFromSkew, skewReloadPending } from './utils/chunkSkew';
48
48
  export { lazyPage } from './utils/lazyPage';
49
+ export { prefetchChunk } from './utils/prefetchChunk';
49
50
  export { CORE_VERSION } from './version';
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.192.0";
49
+ export declare const CORE_VERSION = "0.194.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.192.0';
49
+ export const CORE_VERSION = '0.194.0';