@ihabdevteam/core 0.170.0 → 0.171.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.
@@ -1,4 +1,4 @@
1
- import type { ComponentType } from 'react';
1
+ import { ComponentType } from 'react';
2
2
  import { Context } from 'react';
3
3
  import { CSSProperties } from 'react';
4
4
  import { Dispatch } from 'react';
@@ -10,6 +10,7 @@ import { ForwardRefExoticComponent } from 'react';
10
10
  import type { HTMLAttributes } from 'react';
11
11
  import { JSX } from 'react';
12
12
  import { KeyboardEvent as KeyboardEvent_2 } from 'react';
13
+ import { LazyExoticComponent } from 'react';
13
14
  import { MouseEvent as MouseEvent_2 } from 'react';
14
15
  import { default as React_2 } from 'react';
15
16
  import * as React_3 from 'react';
@@ -1555,7 +1556,7 @@ export declare function copyText(text: string): Promise<boolean>;
1555
1556
  *
1556
1557
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
1557
1558
  */
1558
- export declare const CORE_VERSION = "0.170.0";
1559
+ export declare const CORE_VERSION = "0.171.0";
1559
1560
 
1560
1561
  /** @public */
1561
1562
  declare const coreDefault: Record<string, unknown>;
@@ -3013,6 +3014,29 @@ export declare interface LandingHeaderProps {
3013
3014
  };
3014
3015
  }
3015
3016
 
3017
+ /**
3018
+ * 화면을 늦게 받아 오는 자리 — `lazy()` 를 그냥 쓰지 않고 이걸 쓴다.
3019
+ *
3020
+ * 맨 `lazy(() => import(…))` 는 **모듈이 undefined 로 와도 그대로 `.default` 를 읽는다.**
3021
+ * 그 일이 실제로 난다: 배포 skew 복구가 `vite:preloadError` 를 `preventDefault()` 하면
3022
+ * Vite 헬퍼의 `catch` 가 아무것도 안 던지고 undefined 로 resolve 한다(자세한 것은
3023
+ * `chunkSkew.ts`). 그러면 새로고침이 이미 걸려 있는데도 그 틈에 React 가
3024
+ * `undefined.default` 를 읽어 **오류 화면이 한 번 뜨고 가짜 보고가 남는다.**
3025
+ *
3026
+ * 두 가지를 한다.
3027
+ * 1. **새로고침이 예약돼 있으면 아무것도 결정하지 않는다** — 영영 resolve 하지 않는
3028
+ * 약속을 돌려 Suspense 를 그대로 둔다. 곧 새 판이 뜬다.
3029
+ * 2. 그게 아닌데 모듈이 이상하면 **어느 화면인지 적어** 던진다. 지금까지 이 오류는
3030
+ * 스택이 통째로 vendor-react 안이라(minify 된 `q`) 어느 화면에서 났는지 알 수 없었다.
3031
+ *
3032
+ * Vite 가 아닌 번들러(Next 등)에서는 `vite:preloadError` 가 오지 않으니 이 감싸개는
3033
+ * "모듈이 이상하면 어느 화면인지 적어 던진다" 만 한다 — 그것만으로도 맨 `lazy` 보다 낫다.
3034
+ * @public
3035
+ */
3036
+ export declare function lazyPage<T extends ComponentType<any>>(name: string, load: () => Promise<{
3037
+ default: T;
3038
+ } | undefined>): LazyExoticComponent<T>;
3039
+
3016
3040
  declare type LeaderboardFn = (gameKey: string, limit?: number) => Promise<LeaderboardRow[] | null>;
3017
3041
 
3018
3042
  /**
@@ -5029,6 +5053,44 @@ export declare interface Recommendation {
5029
5053
  */
5030
5054
  export declare function recommendTables(tables: readonly TableCandidate[], weakLabels: readonly string[], limit?: number): Recommendation[];
5031
5055
 
5056
+ /**
5057
+ * 배포 skew 복구 — 그리고 그 복구가 내던 오류 화면.
5058
+ *
5059
+ * ── 무엇이 문제였나 ──
5060
+ * 새 배포로 청크 해시가 바뀌면 캐시된 옛 index.html 이 사라진 청크를 부른다. Vite 는
5061
+ * `vite:preloadError` 를 던지고, 우리는 그때 한 번 새로고침해 새 index.html 을 받는다.
5062
+ * 여기까지는 뜻대로 돈다.
5063
+ *
5064
+ * 그런데 그 이벤트를 `preventDefault()` 하면 Vite 헬퍼가 이렇게 한다(vite 소스 그대로):
5065
+ *
5066
+ * function handlePreloadError(err) {
5067
+ * const e = new Event('vite:preloadError', { cancelable: true })
5068
+ * window.dispatchEvent(e)
5069
+ * if (!e.defaultPrevented) throw err // ← 막으면 안 던진다
5070
+ * }
5071
+ * return baseModule().catch(handlePreloadError) // ← 안 던지니 undefined 로 resolve
5072
+ *
5073
+ * **`location.reload()` 는 곧바로 멈추지 않는다.** 새 판이 뜨기 전까지 React 는 계속
5074
+ * 그리고, `React.lazy` 는 resolve 된 `undefined` 의 `.default` 를 읽는다 —
5075
+ * `Cannot read properties of undefined (reading 'default')`. 운영에서 이 오류가
5076
+ * 07-09 ~ 08-28 동안 **아홉 사람에게 열세 번** 났다(/today·/customers·/my-word-table·
5077
+ * /word-pairs·/word-management·/training-store — 스택은 전부 vendor-react 의 lazy 초기화).
5078
+ *
5079
+ * 즉 복구는 되는데 그 길목에서 **오류 화면을 한 번 띄우고 가짜 보고를 남기고** 있었다.
5080
+ *
5081
+ * ── 어떻게 고치나 ──
5082
+ * 새로고침을 걸어 둔 동안에는 `lazy` 가 아무것도 결정하지 않게 한다(`lazyPage` 가
5083
+ * 영영 resolve 하지 않는 약속을 돌려 Suspense 를 그대로 둔다). 그러려면 "지금 새로고침이
5084
+ * 예약돼 있나"를 물을 수 있어야 해서, 그 판단을 여기 한 곳에 둔다.
5085
+ */
5086
+ /**
5087
+ * 배포 skew 로 판단되면 1회만 새로고침해 새 index.html 을 받는다.
5088
+ * 앱이 `vite:preloadError`(또는 청크 로드 실패)를 받았을 때 부른다.
5089
+ * @returns 새로고침을 걸었으면 true. 이미 최근에 걸었으면 false(진짜 오류로 넘긴다).
5090
+ * @public
5091
+ */
5092
+ export declare function recoverFromSkew(now?: number): boolean;
5093
+
5032
5094
  /** getBoundingClientRect()만 있으면 되는 최소 인터페이스(주로 이벤트의 currentTarget). */
5033
5095
  declare type Rectable = {
5034
5096
  getBoundingClientRect(): DOMRect;
@@ -5875,6 +5937,15 @@ export declare interface SideNavProps {
5875
5937
  onAppModeChange?: (mode: 'malgwi' | 'manager') => void;
5876
5938
  }
5877
5939
 
5940
+ /**
5941
+ * 새로고침을 걸어 두었고 아직 새 판이 안 떴는가.
5942
+ * @public
5943
+ *
5944
+ * `location.reload()` 가 비동기라 그 사이에 React 가 계속 도는데, 그동안 화면에
5945
+ * 무엇을 그릴지 정하는 데 쓴다 — **아무것도 안 그리는 것이 맞다.** 곧 판이 바뀐다.
5946
+ */
5947
+ export declare function skewReloadPending(now?: number): boolean;
5948
+
5878
5949
  /**
5879
5950
  * 가로/세로 슬라이더. 접근성·키보드 지원을 위해 input[type=range] 기반.
5880
5951
  * 채워진 부분은 --slider-pct CSS 변수로 그라데이션 처리한다.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 배포 skew 복구 — 그리고 그 복구가 내던 오류 화면.
3
+ *
4
+ * ── 무엇이 문제였나 ──
5
+ * 새 배포로 청크 해시가 바뀌면 캐시된 옛 index.html 이 사라진 청크를 부른다. Vite 는
6
+ * `vite:preloadError` 를 던지고, 우리는 그때 한 번 새로고침해 새 index.html 을 받는다.
7
+ * 여기까지는 뜻대로 돈다.
8
+ *
9
+ * 그런데 그 이벤트를 `preventDefault()` 하면 Vite 헬퍼가 이렇게 한다(vite 소스 그대로):
10
+ *
11
+ * function handlePreloadError(err) {
12
+ * const e = new Event('vite:preloadError', { cancelable: true })
13
+ * window.dispatchEvent(e)
14
+ * if (!e.defaultPrevented) throw err // ← 막으면 안 던진다
15
+ * }
16
+ * return baseModule().catch(handlePreloadError) // ← 안 던지니 undefined 로 resolve
17
+ *
18
+ * **`location.reload()` 는 곧바로 멈추지 않는다.** 새 판이 뜨기 전까지 React 는 계속
19
+ * 그리고, `React.lazy` 는 resolve 된 `undefined` 의 `.default` 를 읽는다 —
20
+ * `Cannot read properties of undefined (reading 'default')`. 운영에서 이 오류가
21
+ * 07-09 ~ 08-28 동안 **아홉 사람에게 열세 번** 났다(/today·/customers·/my-word-table·
22
+ * /word-pairs·/word-management·/training-store — 스택은 전부 vendor-react 의 lazy 초기화).
23
+ *
24
+ * 즉 복구는 되는데 그 길목에서 **오류 화면을 한 번 띄우고 가짜 보고를 남기고** 있었다.
25
+ *
26
+ * ── 어떻게 고치나 ──
27
+ * 새로고침을 걸어 둔 동안에는 `lazy` 가 아무것도 결정하지 않게 한다(`lazyPage` 가
28
+ * 영영 resolve 하지 않는 약속을 돌려 Suspense 를 그대로 둔다). 그러려면 "지금 새로고침이
29
+ * 예약돼 있나"를 물을 수 있어야 해서, 그 판단을 여기 한 곳에 둔다.
30
+ */
31
+ /**
32
+ * 배포 skew 로 판단되면 1회만 새로고침해 새 index.html 을 받는다.
33
+ * 앱이 `vite:preloadError`(또는 청크 로드 실패)를 받았을 때 부른다.
34
+ * @returns 새로고침을 걸었으면 true. 이미 최근에 걸었으면 false(진짜 오류로 넘긴다).
35
+ * @public
36
+ */
37
+ export declare function recoverFromSkew(now?: number): boolean;
38
+ /**
39
+ * 새로고침을 걸어 두었고 아직 새 판이 안 떴는가.
40
+ * @public
41
+ *
42
+ * `location.reload()` 가 비동기라 그 사이에 React 가 계속 도는데, 그동안 화면에
43
+ * 무엇을 그릴지 정하는 데 쓴다 — **아무것도 안 그리는 것이 맞다.** 곧 판이 바뀐다.
44
+ */
45
+ export declare function skewReloadPending(now?: number): boolean;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 배포 skew 복구 — 그리고 그 복구가 내던 오류 화면.
3
+ *
4
+ * ── 무엇이 문제였나 ──
5
+ * 새 배포로 청크 해시가 바뀌면 캐시된 옛 index.html 이 사라진 청크를 부른다. Vite 는
6
+ * `vite:preloadError` 를 던지고, 우리는 그때 한 번 새로고침해 새 index.html 을 받는다.
7
+ * 여기까지는 뜻대로 돈다.
8
+ *
9
+ * 그런데 그 이벤트를 `preventDefault()` 하면 Vite 헬퍼가 이렇게 한다(vite 소스 그대로):
10
+ *
11
+ * function handlePreloadError(err) {
12
+ * const e = new Event('vite:preloadError', { cancelable: true })
13
+ * window.dispatchEvent(e)
14
+ * if (!e.defaultPrevented) throw err // ← 막으면 안 던진다
15
+ * }
16
+ * return baseModule().catch(handlePreloadError) // ← 안 던지니 undefined 로 resolve
17
+ *
18
+ * **`location.reload()` 는 곧바로 멈추지 않는다.** 새 판이 뜨기 전까지 React 는 계속
19
+ * 그리고, `React.lazy` 는 resolve 된 `undefined` 의 `.default` 를 읽는다 —
20
+ * `Cannot read properties of undefined (reading 'default')`. 운영에서 이 오류가
21
+ * 07-09 ~ 08-28 동안 **아홉 사람에게 열세 번** 났다(/today·/customers·/my-word-table·
22
+ * /word-pairs·/word-management·/training-store — 스택은 전부 vendor-react 의 lazy 초기화).
23
+ *
24
+ * 즉 복구는 되는데 그 길목에서 **오류 화면을 한 번 띄우고 가짜 보고를 남기고** 있었다.
25
+ *
26
+ * ── 어떻게 고치나 ──
27
+ * 새로고침을 걸어 둔 동안에는 `lazy` 가 아무것도 결정하지 않게 한다(`lazyPage` 가
28
+ * 영영 resolve 하지 않는 약속을 돌려 Suspense 를 그대로 둔다). 그러려면 "지금 새로고침이
29
+ * 예약돼 있나"를 물을 수 있어야 해서, 그 판단을 여기 한 곳에 둔다.
30
+ */
31
+ const SKEW_KEY = 'chunk-reload-ts';
32
+ /** 이 시간 안에 다시 실패하면 배포 skew 가 아니라 진짜 오류로 본다(무한 새로고침 방지). */
33
+ const COOLDOWN_MS = 10_000;
34
+ function 마지막시각() {
35
+ try {
36
+ return Number(sessionStorage.getItem(SKEW_KEY) || 0);
37
+ }
38
+ catch {
39
+ /* 사생활 보호 모드 등에서 sessionStorage 가 막힌다. 못 읽으면 '없음' 으로 본다 —
40
+ 그러면 새로고침을 한 번 더 하게 될 뿐, 화면이 멈추지는 않는다. */
41
+ return 0;
42
+ }
43
+ }
44
+ /**
45
+ * 배포 skew 로 판단되면 1회만 새로고침해 새 index.html 을 받는다.
46
+ * 앱이 `vite:preloadError`(또는 청크 로드 실패)를 받았을 때 부른다.
47
+ * @returns 새로고침을 걸었으면 true. 이미 최근에 걸었으면 false(진짜 오류로 넘긴다).
48
+ * @public
49
+ */
50
+ export function recoverFromSkew(now = Date.now()) {
51
+ if (now - 마지막시각() <= COOLDOWN_MS)
52
+ return false;
53
+ try {
54
+ sessionStorage.setItem(SKEW_KEY, String(now));
55
+ }
56
+ catch { /* 못 써도 새로고침은 한다 */ }
57
+ window.location.reload();
58
+ return true;
59
+ }
60
+ /**
61
+ * 새로고침을 걸어 두었고 아직 새 판이 안 떴는가.
62
+ * @public
63
+ *
64
+ * `location.reload()` 가 비동기라 그 사이에 React 가 계속 도는데, 그동안 화면에
65
+ * 무엇을 그릴지 정하는 데 쓴다 — **아무것도 안 그리는 것이 맞다.** 곧 판이 바뀐다.
66
+ */
67
+ export function skewReloadPending(now = Date.now()) {
68
+ const last = 마지막시각();
69
+ return last > 0 && now - last <= COOLDOWN_MS;
70
+ }
@@ -0,0 +1,23 @@
1
+ import { type ComponentType, type LazyExoticComponent } from 'react';
2
+ /**
3
+ * 화면을 늦게 받아 오는 자리 — `lazy()` 를 그냥 쓰지 않고 이걸 쓴다.
4
+ *
5
+ * 맨 `lazy(() => import(…))` 는 **모듈이 undefined 로 와도 그대로 `.default` 를 읽는다.**
6
+ * 그 일이 실제로 난다: 배포 skew 복구가 `vite:preloadError` 를 `preventDefault()` 하면
7
+ * Vite 헬퍼의 `catch` 가 아무것도 안 던지고 undefined 로 resolve 한다(자세한 것은
8
+ * `chunkSkew.ts`). 그러면 새로고침이 이미 걸려 있는데도 그 틈에 React 가
9
+ * `undefined.default` 를 읽어 **오류 화면이 한 번 뜨고 가짜 보고가 남는다.**
10
+ *
11
+ * 두 가지를 한다.
12
+ * 1. **새로고침이 예약돼 있으면 아무것도 결정하지 않는다** — 영영 resolve 하지 않는
13
+ * 약속을 돌려 Suspense 를 그대로 둔다. 곧 새 판이 뜬다.
14
+ * 2. 그게 아닌데 모듈이 이상하면 **어느 화면인지 적어** 던진다. 지금까지 이 오류는
15
+ * 스택이 통째로 vendor-react 안이라(minify 된 `q`) 어느 화면에서 났는지 알 수 없었다.
16
+ *
17
+ * Vite 가 아닌 번들러(Next 등)에서는 `vite:preloadError` 가 오지 않으니 이 감싸개는
18
+ * "모듈이 이상하면 어느 화면인지 적어 던진다" 만 한다 — 그것만으로도 맨 `lazy` 보다 낫다.
19
+ * @public
20
+ */
21
+ export declare function lazyPage<T extends ComponentType<any>>(name: string, load: () => Promise<{
22
+ default: T;
23
+ } | undefined>): LazyExoticComponent<T>;
@@ -0,0 +1,45 @@
1
+ import { lazy } from 'react';
2
+ import { skewReloadPending } from './chunkSkew';
3
+ /**
4
+ * 화면을 늦게 받아 오는 자리 — `lazy()` 를 그냥 쓰지 않고 이걸 쓴다.
5
+ *
6
+ * 맨 `lazy(() => import(…))` 는 **모듈이 undefined 로 와도 그대로 `.default` 를 읽는다.**
7
+ * 그 일이 실제로 난다: 배포 skew 복구가 `vite:preloadError` 를 `preventDefault()` 하면
8
+ * Vite 헬퍼의 `catch` 가 아무것도 안 던지고 undefined 로 resolve 한다(자세한 것은
9
+ * `chunkSkew.ts`). 그러면 새로고침이 이미 걸려 있는데도 그 틈에 React 가
10
+ * `undefined.default` 를 읽어 **오류 화면이 한 번 뜨고 가짜 보고가 남는다.**
11
+ *
12
+ * 두 가지를 한다.
13
+ * 1. **새로고침이 예약돼 있으면 아무것도 결정하지 않는다** — 영영 resolve 하지 않는
14
+ * 약속을 돌려 Suspense 를 그대로 둔다. 곧 새 판이 뜬다.
15
+ * 2. 그게 아닌데 모듈이 이상하면 **어느 화면인지 적어** 던진다. 지금까지 이 오류는
16
+ * 스택이 통째로 vendor-react 안이라(minify 된 `q`) 어느 화면에서 났는지 알 수 없었다.
17
+ *
18
+ * Vite 가 아닌 번들러(Next 등)에서는 `vite:preloadError` 가 오지 않으니 이 감싸개는
19
+ * "모듈이 이상하면 어느 화면인지 적어 던진다" 만 한다 — 그것만으로도 맨 `lazy` 보다 낫다.
20
+ * @public
21
+ */
22
+ /* 서명은 React 의 `lazy` 를 그대로 따른다 — 화면마다 props 가 달라서, 여기서 좁히면
23
+ 쓰는 쪽이 전부 `Property 'userId' does not exist` 로 막힌다(실제로 그렇게 됐다).
24
+ React 자신도 같은 이유로 이 자리에 any 를 쓴다(@types/react 의 lazy 서명). */
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ export function lazyPage(name,
27
+ /* 반환 타입에 undefined 를 적어 둔다 — 실제로 그렇게 온다(아래 참고). 타입이
28
+ "그럴 리 없다"고 말하면 부르는 쪽이 `.then(m => ({ default: m.X }))` 처럼 모듈을
29
+ 그대로 읽는 코드를 쓰게 되고, 그러면 여기 닿기도 전에 터진다. */
30
+ load) {
31
+ return lazy(async () => {
32
+ /* **undefined 로 resolve 될 수 있다.** 타입은 그럴 리 없다고 하지만 실제로 그렇게 온다
33
+ (배포 skew 복구가 preventDefault 한 뒤). 타입을 믿지 말고 확인한다. */
34
+ const mod = await load();
35
+ if (mod && typeof mod === 'object' && 'default' in mod) {
36
+ return mod;
37
+ }
38
+ /* 여기 오는 길은 사실상 하나뿐이다 — 청크를 못 받아 복구가 도는 중. 새 판을
39
+ 기다린다. 영영 resolve 하지 않는 약속을 돌려 React 가 이 경계를 건드리지 않게 한다. */
40
+ if (skewReloadPending())
41
+ return new Promise(() => { });
42
+ throw new Error(`[lazyPage] ${name}: 화면 모듈을 받지 못했습니다(default 내보내기 없음). ` +
43
+ '배포 직후라면 새로고침으로 풀립니다.');
44
+ });
45
+ }
@@ -55,4 +55,6 @@ export { wordContainsChoseong, wordContainsJungseong, wordContainsJongseong, wor
55
55
  export { isSyllablePattern, matchesSyllablePattern, validateSyllableSearchInput, patternToComposition, matchesWordQuery, searchAndRankWords } from './utils/koreanSyllable';
56
56
  export { PLAN_FEATURES, PLAN_LIMITS, TIER_RANK, getPlanTier, isManagerTier, type PlanTier, type DataLevel, type PlanFeatures, type PlanLimits, } from './utils/planFeatures';
57
57
  export { WORD_SUGGESTION_WEEKLY_QUOTA, WORD_SUGGESTION_ACCOUNT_LIMIT, WORD_SUGGESTION_EXISTING_ERROR, isThisWeekPick, } from './utils/wordSuggestionPolicy';
58
+ export { recoverFromSkew, skewReloadPending } from './utils/chunkSkew';
59
+ export { lazyPage } from './utils/lazyPage';
58
60
  export { CORE_VERSION } from './version';
@@ -44,4 +44,6 @@ export { PLAN_FEATURES, PLAN_LIMITS, TIER_RANK, getPlanTier, isManagerTier, } fr
44
44
  말귀어드민(단어 제안 관리)이 같은 숫자를 봐야 하는데, 한쪽 화면 폴더에 두면
45
45
  다른 쪽이 그 폴더 없이는 빌드되지 않는다. 값과 순수 함수뿐이다. */
46
46
  export { WORD_SUGGESTION_WEEKLY_QUOTA, WORD_SUGGESTION_ACCOUNT_LIMIT, WORD_SUGGESTION_EXISTING_ERROR, isThisWeekPick, } from './utils/wordSuggestionPolicy';
47
+ export { recoverFromSkew, skewReloadPending } from './utils/chunkSkew';
48
+ export { lazyPage } from './utils/lazyPage';
47
49
  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.170.0";
49
+ export declare const CORE_VERSION = "0.171.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.170.0';
49
+ export const CORE_VERSION = '0.171.0';