@ihabdevteam/core 0.169.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.
- package/CHANGELOG.md +14 -0
- package/dist/components.cjs.js +174 -174
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +185 -185
- package/dist/ihabdevteam-core.d.ts +83 -5
- package/dist/packages/core/src/components/BarChart.d.ts +1 -1
- package/dist/packages/core/src/components/BarChart.js +2 -2
- package/dist/packages/core/src/components/ChartSection.d.ts +4 -1
- package/dist/packages/core/src/components/ChartSection.js +16 -2
- package/dist/packages/core/src/components/RingChart.d.ts +3 -1
- package/dist/packages/core/src/components/RingChart.js +2 -2
- package/dist/packages/core/src/components/barChartTypes.d.ts +2 -0
- package/dist/packages/core/src/utils/chunkSkew.d.ts +45 -0
- package/dist/packages/core/src/utils/chunkSkew.js +70 -0
- package/dist/packages/core/src/utils/lazyPage.d.ts +23 -0
- package/dist/packages/core/src/utils/lazyPage.js +45 -0
- package/dist/packages/core/src/utils.d.ts +2 -0
- package/dist/packages/core/src/utils.js +2 -0
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +2 -2
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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';
|
|
@@ -309,7 +310,7 @@ export declare type BadgeType = 'bar' | 'dot';
|
|
|
309
310
|
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(월~일) 막대
|
|
310
311
|
* 원시 렌더링은 내부 BarChartCanvas가 담당한다.
|
|
311
312
|
*/
|
|
312
|
-
export declare function BarChart({ size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, rateAgg, tightMax, avgMode, totalFormat, series, seriesMode, loading, onDetail, headExtra, headSegment, icon, disc, onClick, cardAriaLabel, detailSize, barGap, baseline, className, inGrid, }: BarChartProps): JSX.Element;
|
|
313
|
+
export declare function BarChart({ size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, rateAgg, tightMax, avgMode, totalFormat, series, seriesMode, loading, onDetail, headExtra, headSegment, icon, disc, onClick, cardAriaLabel, cardTooltip, detailSize, barGap, baseline, className, inGrid, }: BarChartProps): JSX.Element;
|
|
313
314
|
|
|
314
315
|
/** @public */
|
|
315
316
|
export declare interface BarChartControl {
|
|
@@ -417,6 +418,8 @@ export declare interface BarChartProps {
|
|
|
417
418
|
onClick?: () => void;
|
|
418
419
|
/** 누르는 카드의 이름(낭독기) — 안 주면 제목 */
|
|
419
420
|
cardAriaLabel?: string;
|
|
421
|
+
/** 카드에 포인터가 닿을 때(키보드는 초점) 뜨는 설명 — StatChart tooltip 과 같은 뜻 */
|
|
422
|
+
cardTooltip?: string;
|
|
420
423
|
/** 제목 왼쪽 아이콘 — Section·ChartSection 과 같은 자리·같은 색 */
|
|
421
424
|
icon?: string;
|
|
422
425
|
/** 제목 아래 설명 한 줄 */
|
|
@@ -1053,7 +1056,7 @@ export declare interface ChartPoint {
|
|
|
1053
1056
|
* 공유하도록 두 컴포넌트가 내부적으로 사용한다.
|
|
1054
1057
|
* @public
|
|
1055
1058
|
*/
|
|
1056
|
-
export declare function ChartSection({ title, titleClassName, icon, disc, headRight, headSegment, headMenuItems, collapseBelow, onClick, ariaLabel, subRow, className, inGrid, children }: ChartSectionProps): JSX.Element;
|
|
1059
|
+
export declare function ChartSection({ title, titleClassName, icon, disc, headRight, headSegment, headMenuItems, collapseBelow, onClick, ariaLabel, tooltip, subRow, className, inGrid, children }: ChartSectionProps): JSX.Element;
|
|
1057
1060
|
|
|
1058
1061
|
/** @public */
|
|
1059
1062
|
export declare interface ChartSectionProps {
|
|
@@ -1081,6 +1084,9 @@ export declare interface ChartSectionProps {
|
|
|
1081
1084
|
onClick?: () => void;
|
|
1082
1085
|
/** 누르는 카드의 이름(낭독기) — 안 주면 제목이 글자일 때 그것을 쓴다 */
|
|
1083
1086
|
ariaLabel?: string;
|
|
1087
|
+
/** 카드에 포인터가 닿을 때(키보드는 초점이 들어올 때) 뜨는 설명 — `StatChart tooltip` 과 같은 뜻.
|
|
1088
|
+
* 같은 줄에 선 StatChart 카드들만 설명이 뜨고 이 카드만 안 뜨던 것을 메운다. */
|
|
1089
|
+
tooltip?: string;
|
|
1084
1090
|
/** 총계(ChartTotal) + 범례(ChartLegend)를 담는 서브로우 */
|
|
1085
1091
|
subRow?: ReactNode;
|
|
1086
1092
|
/** 섹션 루트에 추가할 클래스 */
|
|
@@ -1550,7 +1556,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1550
1556
|
*
|
|
1551
1557
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1552
1558
|
*/
|
|
1553
|
-
export declare const CORE_VERSION = "0.
|
|
1559
|
+
export declare const CORE_VERSION = "0.171.0";
|
|
1554
1560
|
|
|
1555
1561
|
/** @public */
|
|
1556
1562
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -3008,6 +3014,29 @@ export declare interface LandingHeaderProps {
|
|
|
3008
3014
|
};
|
|
3009
3015
|
}
|
|
3010
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
|
+
|
|
3011
3040
|
declare type LeaderboardFn = (gameKey: string, limit?: number) => Promise<LeaderboardRow[] | null>;
|
|
3012
3041
|
|
|
3013
3042
|
/**
|
|
@@ -5024,6 +5053,44 @@ export declare interface Recommendation {
|
|
|
5024
5053
|
*/
|
|
5025
5054
|
export declare function recommendTables(tables: readonly TableCandidate[], weakLabels: readonly string[], limit?: number): Recommendation[];
|
|
5026
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
|
+
|
|
5027
5094
|
/** getBoundingClientRect()만 있으면 되는 최소 인터페이스(주로 이벤트의 currentTarget). */
|
|
5028
5095
|
declare type Rectable = {
|
|
5029
5096
|
getBoundingClientRect(): DOMRect;
|
|
@@ -5341,7 +5408,7 @@ export declare function reviewTableName(original: string | ReviewSource, today:
|
|
|
5341
5408
|
* 미지정 시에는 헤더 없는 순수 링만 렌더된다(크기/색상 데모 등).
|
|
5342
5409
|
* @public
|
|
5343
5410
|
*/
|
|
5344
|
-
export declare function RingChart({ titleKey, headRight, icon, showTotal, totalLabelKey, legend, loading, className, inGrid, onClick, ariaLabel, ...ringProps }: RingChartProps): JSX.Element;
|
|
5411
|
+
export declare function RingChart({ titleKey, headRight, icon, showTotal, totalLabelKey, legend, loading, className, inGrid, onClick, ariaLabel, tooltip, ...ringProps }: RingChartProps): JSX.Element;
|
|
5345
5412
|
|
|
5346
5413
|
/** @public */
|
|
5347
5414
|
export declare interface RingChartProps {
|
|
@@ -5398,6 +5465,8 @@ export declare interface RingChartProps {
|
|
|
5398
5465
|
onClick?: () => void;
|
|
5399
5466
|
/** 누르는 카드의 이름(낭독기) — 안 주면 제목 */
|
|
5400
5467
|
ariaLabel?: string;
|
|
5468
|
+
/** 카드에 포인터가 닿을 때(키보드는 초점) 뜨는 설명 — StatChart tooltip 과 같은 뜻(titleKey 지정 시에만) */
|
|
5469
|
+
tooltip?: string;
|
|
5401
5470
|
}
|
|
5402
5471
|
|
|
5403
5472
|
/** @public */
|
|
@@ -5868,6 +5937,15 @@ export declare interface SideNavProps {
|
|
|
5868
5937
|
onAppModeChange?: (mode: 'malgwi' | 'manager') => void;
|
|
5869
5938
|
}
|
|
5870
5939
|
|
|
5940
|
+
/**
|
|
5941
|
+
* 새로고침을 걸어 두었고 아직 새 판이 안 떴는가.
|
|
5942
|
+
* @public
|
|
5943
|
+
*
|
|
5944
|
+
* `location.reload()` 가 비동기라 그 사이에 React 가 계속 도는데, 그동안 화면에
|
|
5945
|
+
* 무엇을 그릴지 정하는 데 쓴다 — **아무것도 안 그리는 것이 맞다.** 곧 판이 바뀐다.
|
|
5946
|
+
*/
|
|
5947
|
+
export declare function skewReloadPending(now?: number): boolean;
|
|
5948
|
+
|
|
5871
5949
|
/**
|
|
5872
5950
|
* 가로/세로 슬라이더. 접근성·키보드 지원을 위해 input[type=range] 기반.
|
|
5873
5951
|
* 채워진 부분은 --slider-pct CSS 변수로 그라데이션 처리한다.
|
|
@@ -13,4 +13,4 @@ import type { BarChartProps } from './barChartTypes';
|
|
|
13
13
|
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(월~일) 막대
|
|
14
14
|
* 원시 렌더링은 내부 BarChartCanvas가 담당한다.
|
|
15
15
|
*/
|
|
16
|
-
export default function BarChart({ size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, rateAgg, tightMax, avgMode, totalFormat, series, seriesMode, loading, onDetail, headExtra, headSegment, icon, disc, onClick, cardAriaLabel, detailSize, barGap, baseline, className, inGrid, }: BarChartProps): import("react").JSX.Element;
|
|
16
|
+
export default function BarChart({ size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, rateAgg, tightMax, avgMode, totalFormat, series, seriesMode, loading, onDetail, headExtra, headSegment, icon, disc, onClick, cardAriaLabel, cardTooltip, detailSize, barGap, baseline, className, inGrid, }: BarChartProps): import("react").JSX.Element;
|
|
@@ -261,7 +261,7 @@ const BAR_SIZE_PRESET = {
|
|
|
261
261
|
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(월~일) 막대
|
|
262
262
|
* 원시 렌더링은 내부 BarChartCanvas가 담당한다.
|
|
263
263
|
*/
|
|
264
|
-
export default function BarChart({ size = 'medium', barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal = false, totalUnitKey, totalLabelKey, control: controlProp, hideControls = false, rate = false, rateAgg = 'dayMean', tightMax = false, avgMode = false, totalFormat, series, seriesMode = 'stack', loading = false, onDetail, headExtra, headSegment, icon, disc, onClick, cardAriaLabel, detailSize = 32, barGap, baseline, className, inGrid = false, }) {
|
|
264
|
+
export default function BarChart({ size = 'medium', barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal = false, totalUnitKey, totalLabelKey, control: controlProp, hideControls = false, rate = false, rateAgg = 'dayMean', tightMax = false, avgMode = false, totalFormat, series, seriesMode = 'stack', loading = false, onDetail, headExtra, headSegment, icon, disc, onClick, cardAriaLabel, cardTooltip, detailSize = 32, barGap, baseline, className, inGrid = false, }) {
|
|
265
265
|
const { t, i18n } = useTranslation();
|
|
266
266
|
const sizePreset = BAR_SIZE_PRESET[size];
|
|
267
267
|
// 외부 control 미지정 시 내부 컨트롤 사용 (단독 차트 하위 호환)
|
|
@@ -397,7 +397,7 @@ export default function BarChart({ size = 'medium', barWidth, titleKey, data, co
|
|
|
397
397
|
const isLarge = size === 'large';
|
|
398
398
|
const sectionClassName = [isSmall ? 'manager-section--compact' : isLarge ? 'manager-section--full' : undefined, className]
|
|
399
399
|
.filter(Boolean).join(' ') || undefined;
|
|
400
|
-
return (_jsx(ChartSection, { className: sectionClassName, inGrid: inGrid, icon: icon, disc: disc, onClick: onClick, ariaLabel: cardAriaLabel, headSegment: headSegment,
|
|
400
|
+
return (_jsx(ChartSection, { className: sectionClassName, inGrid: inGrid, icon: icon, disc: disc, onClick: onClick, ariaLabel: cardAriaLabel, tooltip: cardTooltip, headSegment: headSegment,
|
|
401
401
|
/* 좁아 고르개가 '⋯' 로 접히면 상세보기도 그 메뉴로 들어간다 — 머리에 단추 둘이 남지 않게 */
|
|
402
402
|
headMenuItems: onDetail ? [{ label: t('trainingStatus.detail', '상세보기'), icon: 'list', onClick: onDetail, closeOnSelect: true }] : undefined, title: t(titleKey), titleClassName: isSmall ? 'h5 chart-section-title--small' : undefined, headRight: isSmall ? (headExtra ?? undefined) : ({ collapsed }) => _jsxs(_Fragment, { children: [!hideControls && _jsx(BarChartControls, { control: control }), headExtra, onDetail && !collapsed && (_jsx("span", { className: "manager-bar-chart-detail-btn", children: _jsx(Button, { htmlType: "button", size: detailSize, type: "dot", icon: "list", ariaLabel: t('trainingStatus.detail', '상세보기'), showTooltipInfo: t('trainingStatus.detail', '상세보기'), onClick: onDetail }) }))] }), subRow: _jsxs(_Fragment, { children: [showTotal ? ((rate || avgMode || (!!totalLabelKey && !!totalFormat)) ? (
|
|
403
403
|
// 진행률/평균 모드 또는 totalLabelKey+totalFormat 조합: 라벨(윗줄) + 값(아랫줄). size='small'은 라벨 생략
|
|
@@ -61,6 +61,9 @@ export interface ChartSectionProps {
|
|
|
61
61
|
onClick?: () => void;
|
|
62
62
|
/** 누르는 카드의 이름(낭독기) — 안 주면 제목이 글자일 때 그것을 쓴다 */
|
|
63
63
|
ariaLabel?: string;
|
|
64
|
+
/** 카드에 포인터가 닿을 때(키보드는 초점이 들어올 때) 뜨는 설명 — `StatChart tooltip` 과 같은 뜻.
|
|
65
|
+
* 같은 줄에 선 StatChart 카드들만 설명이 뜨고 이 카드만 안 뜨던 것을 메운다. */
|
|
66
|
+
tooltip?: string;
|
|
64
67
|
/** 총계(ChartTotal) + 범례(ChartLegend)를 담는 서브로우 */
|
|
65
68
|
subRow?: ReactNode;
|
|
66
69
|
/** 섹션 루트에 추가할 클래스 */
|
|
@@ -80,4 +83,4 @@ export interface ChartSectionProps {
|
|
|
80
83
|
* 공유하도록 두 컴포넌트가 내부적으로 사용한다.
|
|
81
84
|
* @public
|
|
82
85
|
*/
|
|
83
|
-
export default function ChartSection({ title, titleClassName, icon, disc, headRight, headSegment, headMenuItems, collapseBelow, onClick, ariaLabel, subRow, className, inGrid, children }: ChartSectionProps): import("react").JSX.Element;
|
|
86
|
+
export default function ChartSection({ title, titleClassName, icon, disc, headRight, headSegment, headMenuItems, collapseBelow, onClick, ariaLabel, tooltip, subRow, className, inGrid, children }: ChartSectionProps): import("react").JSX.Element;
|
|
@@ -7,6 +7,8 @@ import Icon from './Icon';
|
|
|
7
7
|
import Button from './Button';
|
|
8
8
|
import SegmentedControl from './SegmentedControl';
|
|
9
9
|
import Tooltip from './Tooltip/Tooltip';
|
|
10
|
+
import TooltipInfo from './Tooltip/InfoTooltip';
|
|
11
|
+
import { useHoverAnchor } from '../hooks/useHoverAnchor';
|
|
10
12
|
import MenuTooltip from './Tooltip/MenuTooltip';
|
|
11
13
|
/** 차트 범례 — 색상 점 + 라벨 목록. BarChart/RingChart 섹션이 공유. @public */
|
|
12
14
|
export function ChartLegend({ items, className }) {
|
|
@@ -25,9 +27,14 @@ export function ChartTotal({ label, children }) {
|
|
|
25
27
|
* 공유하도록 두 컴포넌트가 내부적으로 사용한다.
|
|
26
28
|
* @public
|
|
27
29
|
*/
|
|
28
|
-
export default function ChartSection({ title, titleClassName = 'h3', icon, disc, headRight, headSegment, headMenuItems, collapseBelow = 420, onClick, ariaLabel, subRow, className, inGrid = false, children }) {
|
|
30
|
+
export default function ChartSection({ title, titleClassName = 'h3', icon, disc, headRight, headSegment, headMenuItems, collapseBelow = 420, onClick, ariaLabel, tooltip, subRow, className, inGrid = false, children }) {
|
|
29
31
|
const { t } = useTranslation();
|
|
30
32
|
const secRef = useRef(null);
|
|
33
|
+
/* 설명 툴팁 — StatChart 와 같은 결(카드 사각형 기준, 포인터가 나가거나 초점이 빠지면 닫힘).
|
|
34
|
+
hover 전용이면 키보드로는 볼 수 없어 초점에도 연다. */
|
|
35
|
+
const { anchorRect: 설명자리, openAt: 설명열기, close: 설명닫기 } = useHoverAnchor();
|
|
36
|
+
const 설명보이기 = useCallback(() => { if (tooltip && secRef.current)
|
|
37
|
+
설명열기(secRef.current); }, [tooltip, 설명열기]);
|
|
31
38
|
/* 좁은지는 **카드 제 폭** 으로 잰다 — 부르는 쪽이 격자 칸 폭으로 대신 재면 두 칸을 차지하는 카드에서 틀린다.
|
|
32
39
|
ResizeObserver 가 없는 환경(구형·SSR)에서는 접지 않는다(넓은 쪽이 기본). */
|
|
33
40
|
const [좁음, set좁음] = useState(false);
|
|
@@ -87,5 +94,12 @@ export default function ChartSection({ title, titleClassName = 'h3', icon, disc,
|
|
|
87
94
|
}, [onClick]);
|
|
88
95
|
const 오른쪽 = typeof headRight === 'function' ? headRight({ collapsed: 접힘 }) : headRight;
|
|
89
96
|
const 머리오른쪽 = (오른쪽 || headSegment) ? (_jsxs("div", { className: "manager-section__head-right", children: [오른쪽, headSegment && (접힘 ? (_jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "ellipsis", ariaLabel: headSegment.ariaLabel ?? t('common.more', '더보기'), showTooltipInfo: headSegment.ariaLabel ?? t('common.more', '더보기'), "aria-haspopup": "menu", "aria-expanded": !!메뉴자리, onClick: 메뉴열기 })) : (_jsx(SegmentedControl, { size: 32, ariaLabel: headSegment.ariaLabel, options: headSegment.options.map(o => ({ key: o.key, label: o.label, disabled: o.disabled })), value: headSegment.value, onChange: headSegment.onChange })))] })) : null;
|
|
90
|
-
return (_jsxs("section", { ref: secRef, className: ['manager-section', inGrid && 'manager-section--grid', onClick && 'manager-section--clickable', className].filter(Boolean).join(' '), onClick: onClick ? 카드누름 : undefined, onKeyDown: onClick ? 카드키 : undefined,
|
|
97
|
+
return (_jsxs("section", { ref: secRef, className: ['manager-section', inGrid && 'manager-section--grid', onClick && 'manager-section--clickable', className].filter(Boolean).join(' '), onClick: onClick ? 카드누름 : undefined, onKeyDown: onClick ? 카드키 : undefined, onPointerEnter: tooltip ? 설명보이기 : undefined, onPointerLeave: tooltip ? 설명닫기 : undefined, onFocus: tooltip ? 설명보이기 : undefined, onBlur: tooltip ? 설명닫기 : undefined, role: onClick ? 'button' : undefined, tabIndex: onClick ? 0 : undefined, "aria-label": onClick ? (ariaLabel ?? (typeof title === 'string' ? title : undefined)) : undefined, children: [tooltip && 설명자리 && (_jsx(TooltipInfo, { anchorRect: {
|
|
98
|
+
left: 설명자리.left,
|
|
99
|
+
top: 설명자리.top + 21,
|
|
100
|
+
right: 설명자리.right,
|
|
101
|
+
bottom: 설명자리.bottom,
|
|
102
|
+
width: 설명자리.width,
|
|
103
|
+
height: 설명자리.height,
|
|
104
|
+
}, text: tooltip })), _jsxs("div", { className: "manager-section__head", children: [_jsxs("div", { className: "manager-section__title-row", children: [icon && _jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "manager-section__icon" }), _jsx("div", { className: ['manager-section__title', titleClassName].filter(Boolean).join(' '), children: title })] }), 머리오른쪽, disc && _jsx("div", { className: "manager-section__disc body-3", children: disc })] }), subRow && _jsx("div", { className: "barchart-sub-row", children: subRow }), children, 메뉴자리 && (_jsx(Tooltip, { anchorRect: 메뉴자리, onClose: 메뉴닫기, ariaLabel: headSegment?.ariaLabel ?? t('tooltip.menu', '메뉴'), children: _jsx(MenuTooltip, { columns: [{ items: 메뉴항목 }], onClose: 메뉴닫기, size: "small" }) }))] }));
|
|
91
105
|
}
|
|
@@ -64,6 +64,8 @@ export interface RingChartProps {
|
|
|
64
64
|
onClick?: () => void;
|
|
65
65
|
/** 누르는 카드의 이름(낭독기) — 안 주면 제목 */
|
|
66
66
|
ariaLabel?: string;
|
|
67
|
+
/** 카드에 포인터가 닿을 때(키보드는 초점) 뜨는 설명 — StatChart tooltip 과 같은 뜻(titleKey 지정 시에만) */
|
|
68
|
+
tooltip?: string;
|
|
67
69
|
}
|
|
68
70
|
/**
|
|
69
71
|
* 도넛 링 차트 — 전체 대비 완료 비율을 원형 호로 표현.
|
|
@@ -71,4 +73,4 @@ export interface RingChartProps {
|
|
|
71
73
|
* 미지정 시에는 헤더 없는 순수 링만 렌더된다(크기/색상 데모 등).
|
|
72
74
|
* @public
|
|
73
75
|
*/
|
|
74
|
-
export default function RingChart({ titleKey, headRight, icon, showTotal, totalLabelKey, legend, loading, className, inGrid, onClick, ariaLabel, ...ringProps }: RingChartProps): import("react").JSX.Element;
|
|
76
|
+
export default function RingChart({ titleKey, headRight, icon, showTotal, totalLabelKey, legend, loading, className, inGrid, onClick, ariaLabel, tooltip, ...ringProps }: RingChartProps): import("react").JSX.Element;
|
|
@@ -51,7 +51,7 @@ function RingChartCanvas({ done, total, segments, color = 'var(--color-main)', s
|
|
|
51
51
|
* 미지정 시에는 헤더 없는 순수 링만 렌더된다(크기/색상 데모 등).
|
|
52
52
|
* @public
|
|
53
53
|
*/
|
|
54
|
-
export default function RingChart({ titleKey, headRight, icon, showTotal, totalLabelKey, legend, loading, className, inGrid = false, onClick, ariaLabel, ...ringProps }) {
|
|
54
|
+
export default function RingChart({ titleKey, headRight, icon, showTotal, totalLabelKey, legend, loading, className, inGrid = false, onClick, ariaLabel, tooltip, ...ringProps }) {
|
|
55
55
|
const { t } = useTranslation();
|
|
56
56
|
if (!titleKey)
|
|
57
57
|
return _jsx(RingChartCanvas, { ...ringProps });
|
|
@@ -64,5 +64,5 @@ export default function RingChart({ titleKey, headRight, icon, showTotal, totalL
|
|
|
64
64
|
.filter(Boolean).join(' ') || undefined;
|
|
65
65
|
// StatChart(.manager-stat-card__icon)와 동일한 톤 — headRight 자리에 렌더(둘 중 하나만 사용)
|
|
66
66
|
const iconNode = icon && (_jsx("span", { className: "chart-section-icon", children: _jsx(Icon, { icon: icon, className: "chart-section-icon-inner" }) }));
|
|
67
|
-
return (_jsx(ChartSection, { className: sectionClassName, inGrid: inGrid, onClick: onClick, ariaLabel: ariaLabel, title: t(titleKey), titleClassName: isSmall ? 'h5 chart-section-title--small' : undefined, headRight: iconNode || (isSmall ? undefined : headRight), subRow: (showTotal || legend) && _jsxs(_Fragment, { children: [showTotal ? (_jsxs(ChartTotal, { label: !isSmall && totalLabelKey ? t(totalLabelKey) : undefined, children: [_jsx("span", { className: "barchart-total__num h0", children: pct }), _jsx("span", { className: "barchart-total__unit h5", children: "%" })] })) : _jsx("span", {}), legend && _jsx(ChartLegend, { items: legend })] }), children: loading ? (_jsx("div", { className: "manager-bar-chart-loading", children: _jsx(Loader, { size: 32 }) })) : (_jsx("div", { className: "ring-chart-section-body ring-chart-section-body--offset", children: _jsx(RingChartCanvas, { ...ringProps, hideCenter: ringProps.hideCenter ?? isSmall }) })) }));
|
|
67
|
+
return (_jsx(ChartSection, { className: sectionClassName, inGrid: inGrid, onClick: onClick, ariaLabel: ariaLabel, tooltip: tooltip, title: t(titleKey), titleClassName: isSmall ? 'h5 chart-section-title--small' : undefined, headRight: iconNode || (isSmall ? undefined : headRight), subRow: (showTotal || legend) && _jsxs(_Fragment, { children: [showTotal ? (_jsxs(ChartTotal, { label: !isSmall && totalLabelKey ? t(totalLabelKey) : undefined, children: [_jsx("span", { className: "barchart-total__num h0", children: pct }), _jsx("span", { className: "barchart-total__unit h5", children: "%" })] })) : _jsx("span", {}), legend && _jsx(ChartLegend, { items: legend })] }), children: loading ? (_jsx("div", { className: "manager-bar-chart-loading", children: _jsx(Loader, { size: 32 }) })) : (_jsx("div", { className: "ring-chart-section-body ring-chart-section-body--offset", children: _jsx(RingChartCanvas, { ...ringProps, hideCenter: ringProps.hideCenter ?? isSmall }) })) }));
|
|
68
68
|
}
|
|
@@ -103,6 +103,8 @@ export interface BarChartProps {
|
|
|
103
103
|
onClick?: () => void;
|
|
104
104
|
/** 누르는 카드의 이름(낭독기) — 안 주면 제목 */
|
|
105
105
|
cardAriaLabel?: string;
|
|
106
|
+
/** 카드에 포인터가 닿을 때(키보드는 초점) 뜨는 설명 — StatChart tooltip 과 같은 뜻 */
|
|
107
|
+
cardTooltip?: string;
|
|
106
108
|
/** 제목 왼쪽 아이콘 — Section·ChartSection 과 같은 자리·같은 색 */
|
|
107
109
|
icon?: string;
|
|
108
110
|
/** 제목 아래 설명 한 줄 */
|
|
@@ -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';
|