@ihabdevteam/core 0.308.0 → 0.310.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 +73 -0
- package/dist/components.cjs.js +173 -173
- package/dist/components.esm.css +1 -1
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +43 -43
- package/dist/ihabdevteam-core.d.ts +22 -7
- package/dist/packages/core/src/components/CountTile.css +6 -3
- package/dist/packages/core/src/components/CountTile.js +11 -1
- package/dist/packages/core/src/components/Training/CategoryIcon.css +8 -0
- package/dist/packages/core/src/components/Training/CategoryIcon.js +9 -1
- package/dist/packages/core/src/components/Training/TrainingStep.d.ts +9 -2
- package/dist/packages/core/src/components/Training/TrainingStep.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingStepCard.d.ts +4 -1
- package/dist/packages/core/src/components/Training/TrainingStepCard.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingStepCardOrder.d.ts +4 -1
- package/dist/packages/core/src/components/Training/TrainingStepCardOrder.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingStepConfirm.d.ts +4 -1
- package/dist/packages/core/src/components/Training/TrainingStepConfirm.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.d.ts +4 -1
- package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingStepOrderList.d.ts +1 -1
- package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +2 -2
- package/dist/packages/core/src/components/Training/trainingStepOrderListTypes.d.ts +3 -0
- package/dist/packages/core/src/components/Training/trainingStepOrderListTypes.js +0 -4
- package/dist/packages/core/src/utils/lazyPage.js +10 -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
|
@@ -1651,7 +1651,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1651
1651
|
*
|
|
1652
1652
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1653
1653
|
*/
|
|
1654
|
-
export declare const CORE_VERSION = "0.
|
|
1654
|
+
export declare const CORE_VERSION = "0.310.0";
|
|
1655
1655
|
|
|
1656
1656
|
/** @public */
|
|
1657
1657
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -9046,13 +9046,15 @@ export declare function TrainingStepBullet({ state }: {
|
|
|
9046
9046
|
}): JSX.Element;
|
|
9047
9047
|
|
|
9048
9048
|
/** @public */
|
|
9049
|
-
export declare function TrainingStepCard({ index, stepIndex, isLast, title, onDone, onChange, context, selectable, hideLabel, hideAudio, maxCards, playAllGapMs }: TrainingStepCardProps): JSX.Element;
|
|
9049
|
+
export declare function TrainingStepCard({ index, stepIndex, isLast, lockedHint, title, onDone, onChange, context, selectable, hideLabel, hideAudio, maxCards, playAllGapMs }: TrainingStepCardProps): JSX.Element;
|
|
9050
9050
|
|
|
9051
9051
|
/** @public */
|
|
9052
9052
|
export declare type TrainingStepCardProps = {
|
|
9053
9053
|
index?: number;
|
|
9054
9054
|
stepIndex?: number;
|
|
9055
9055
|
isLast?: boolean;
|
|
9056
|
+
/** 잠긴 까닭 한 줄 — `TrainingStep` 으로 그대로 넘긴다. (107): 안 주면 안 선다. (99) 때는 이 겉싸개가 안 넘겨 맨 `TrainingStep` 에서만 줄 수 있었다 */
|
|
9057
|
+
lockedHint?: ReactNode | null;
|
|
9056
9058
|
title?: string;
|
|
9057
9059
|
onDone?: (payload?: unknown) => void;
|
|
9058
9060
|
onChange?: (payload?: unknown) => void;
|
|
@@ -9086,13 +9088,15 @@ export declare type TrainingStepCardProps = {
|
|
|
9086
9088
|
};
|
|
9087
9089
|
|
|
9088
9090
|
/** @public */
|
|
9089
|
-
export declare function TrainingStepConfirm({ index, stepIndex, isLast, title, useTitleFaded, onDone, onChange, answer, wrongAnswers, choices: propChoices, multiSelect, correctCount, useDisabledCovered, choiceIcons, shuffleChoices }: TrainingStepConfirmProps): JSX.Element;
|
|
9091
|
+
export declare function TrainingStepConfirm({ index, stepIndex, isLast, lockedHint, title, useTitleFaded, onDone, onChange, answer, wrongAnswers, choices: propChoices, multiSelect, correctCount, useDisabledCovered, choiceIcons, shuffleChoices }: TrainingStepConfirmProps): JSX.Element;
|
|
9090
9092
|
|
|
9091
9093
|
/** @public */
|
|
9092
9094
|
export declare type TrainingStepConfirmProps = {
|
|
9093
9095
|
index?: number;
|
|
9094
9096
|
stepIndex?: number;
|
|
9095
9097
|
isLast?: boolean;
|
|
9098
|
+
/** 잠긴 까닭 한 줄 — `TrainingStep` 으로 그대로 넘긴다. (107): 안 주면 안 선다. (99) 때는 이 겉싸개가 안 넘겨 맨 `TrainingStep` 에서만 줄 수 있었다 */
|
|
9099
|
+
lockedHint?: ReactNode | null;
|
|
9096
9100
|
title?: string;
|
|
9097
9101
|
useTitleFaded?: boolean;
|
|
9098
9102
|
onDone?: (payload?: TrainingPayload) => void;
|
|
@@ -9120,13 +9124,15 @@ export declare type TrainingStepConfirmProps = {
|
|
|
9120
9124
|
};
|
|
9121
9125
|
|
|
9122
9126
|
/** @public */
|
|
9123
|
-
export declare function TrainingStepConfirmWithText({ index, stepIndex, isLast, title, onDone, onChange, context, demoSelected }: TrainingStepConfirmWithTextProps): JSX.Element;
|
|
9127
|
+
export declare function TrainingStepConfirmWithText({ index, stepIndex, isLast, lockedHint, title, onDone, onChange, context, demoSelected }: TrainingStepConfirmWithTextProps): JSX.Element;
|
|
9124
9128
|
|
|
9125
9129
|
/** @public */
|
|
9126
9130
|
export declare type TrainingStepConfirmWithTextProps = {
|
|
9127
9131
|
index?: number;
|
|
9128
9132
|
stepIndex?: number;
|
|
9129
9133
|
isLast?: boolean;
|
|
9134
|
+
/** 잠긴 까닭 한 줄 — `TrainingStep` 으로 그대로 넘긴다. (107): 안 주면 안 선다. (99) 때는 이 겉싸개가 안 넘겨 맨 `TrainingStep` 에서만 줄 수 있었다 */
|
|
9135
|
+
lockedHint?: ReactNode | null;
|
|
9130
9136
|
title?: string;
|
|
9131
9137
|
onDone?: (payload?: TrainingPayload) => void;
|
|
9132
9138
|
onChange?: (payload?: TrainingPayload) => void;
|
|
@@ -9175,13 +9181,15 @@ export declare type TrainingStepListenProps = {
|
|
|
9175
9181
|
tailMs?: number;
|
|
9176
9182
|
};
|
|
9177
9183
|
|
|
9178
|
-
export declare function TrainingStepOrderList({ index, stepIndex, isLast, title, onDone, onChange, context, }: TrainingStepOrderListProps): JSX.Element;
|
|
9184
|
+
export declare function TrainingStepOrderList({ index, stepIndex, isLast, lockedHint, title, onDone, onChange, context, }: TrainingStepOrderListProps): JSX.Element;
|
|
9179
9185
|
|
|
9180
9186
|
/** @public */
|
|
9181
9187
|
export declare interface TrainingStepOrderListProps {
|
|
9182
9188
|
index?: number;
|
|
9183
9189
|
stepIndex?: number;
|
|
9184
9190
|
isLast?: boolean;
|
|
9191
|
+
/** 잠긴 까닭 한 줄 — `TrainingStep` 으로 그대로 넘긴다. (107): 안 주면 안 선다. (99) 때는 이 겉싸개가 안 넘겨 맨 `TrainingStep` 에서만 줄 수 있었다 */
|
|
9192
|
+
lockedHint?: ReactNode | null;
|
|
9185
9193
|
title?: string;
|
|
9186
9194
|
onDone?: (res: ChangeResult) => void;
|
|
9187
9195
|
onChange?: (res: ChangeResult) => void;
|
|
@@ -9203,8 +9211,15 @@ export declare type TrainingStepProps = {
|
|
|
9203
9211
|
* **왜 아직 못 누르는지 알리는 한 줄** — 잠긴 단계 **바로 다음 차례**에만 선다
|
|
9204
9212
|
* (말귀 요청 (99), 2026-10-08).
|
|
9205
9213
|
*
|
|
9206
|
-
*
|
|
9207
|
-
*
|
|
9214
|
+
* **주는 곳에서만 선다 — 안 주면 아무것도 안 선다**(말귀 요청 (107), 사용자 지시 2026-10-10:
|
|
9215
|
+
* 실행 창의 *"다 들으면 고를 수 있어요."* 를 보고 *"누가 추가했어, 제거해"*). 0.302.0 ~ 0.309.0
|
|
9216
|
+
* 에는 안 줘도 그 말이 기본으로 섰는데, **앱에서는 끌 길이 없었다** — 그 단계는 코어 러너가
|
|
9217
|
+
* 세워서 앱이 이 prop 을 만질 자리가 없다(앱 저장소 0건). 켜고 싶으면 `t('training.answerLockedHint')`
|
|
9218
|
+
* 를 그대로 주면 된다(열쇠는 남겨 두었다). `null` 은 안 준 것과 같다.
|
|
9219
|
+
*
|
|
9220
|
+
* (99) 의 낭독기 쪽 까닭(단추가 진짜 `disabled` 라 초점이 안 가 까닭을 들을 길이 없다)은
|
|
9221
|
+
* 그대로 참이다 — 주면 여전히 `role="status"` 로 선다. 보이는 글 없이 그 길만 지키려면
|
|
9222
|
+
* 부르는 쪽이 시각적으로 숨긴 노드를 주면 된다. 코어가 몰래 세우지는 않는다.
|
|
9208
9223
|
*/
|
|
9209
9224
|
lockedHint?: ReactNode | null;
|
|
9210
9225
|
};
|
|
@@ -27,11 +27,14 @@
|
|
|
27
27
|
color: var(--color-darkGray);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/*
|
|
31
|
-
|
|
32
|
-
.
|
|
30
|
+
/* 반응하는 타일(누르기 · 담긴 훈련 미리보기 · 부르는 쪽 툴팁)의 호버 — **호버 모양은 여기 한 곳**.
|
|
31
|
+
`--clickable` 은 커서만 지킨다. 전에는 `--clickable:hover` 에만 있어 얹기만 하는 타일이
|
|
32
|
+
반응하는 자리인데 반응하는 것처럼 안 보였다(말귀 (105), 2026-10-10). */
|
|
33
|
+
.count-tile--interactive:hover {
|
|
33
34
|
background: var(--color-white);
|
|
35
|
+
border-color: var(--color-lightGray);
|
|
34
36
|
}
|
|
37
|
+
.count-tile--clickable { cursor: pointer; }
|
|
35
38
|
|
|
36
39
|
/* 모바일 — 기본 60×60. 72px일 땐 카드 안에 4개가 못 들어가 3+1로 어정쩡하게 갈라졌다.
|
|
37
40
|
60px이면 4개(60×4 + 6×3 = 258)가 카드 안에 한 줄로 들어간다. */
|
|
@@ -16,7 +16,17 @@ const 건너가는틈_MS = 200;
|
|
|
16
16
|
* @public
|
|
17
17
|
*/
|
|
18
18
|
export default function CountTile({ icon, count, title, plan, tooltip = true, className = '', onClick, onMouseEnter, onMouseLeave }) {
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* **반응하는 타일이면 반응하는 것처럼 보인다**(말귀 요청 (105), 사용자 보고 2026-10-10).
|
|
21
|
+
*
|
|
22
|
+
* 전에는 `onClick` 이 있을 때만 호버 모양을 줬다. 그런데 이 타일이 반응하는 길은 셋이다 —
|
|
23
|
+
* 누르기 · 얹으면 담긴 훈련이 뜸(`tooltip && plan`) · 부르는 쪽이 제 툴팁을 띄움(`onMouseEnter`).
|
|
24
|
+
* 회원 숙제 카드는 셋째 길만 쓰고 매니저 숙제 카드는 둘째 길만 써서, **반응하는 자리인데
|
|
25
|
+
* 반응하는 것처럼 안 보였다.** 호버 모양은 `--interactive` 가 맡고 `--clickable` 은 커서만
|
|
26
|
+
* 지킨다 — 누르는 타일과 얹는 타일이 **같은 호버**를 쓰고 커서만 갈린다.
|
|
27
|
+
*/
|
|
28
|
+
const 반응하나 = Boolean(onClick) || Boolean(onMouseEnter) || (tooltip && Boolean(plan));
|
|
29
|
+
const cls = cx('count-tile', 반응하나 && 'count-tile--interactive', onClick && 'count-tile--clickable', className);
|
|
20
30
|
const ref = useRef(null);
|
|
21
31
|
const [anchorRect, setAnchorRect] = useState(null);
|
|
22
32
|
const 늦춤 = useRef(null);
|
|
@@ -55,6 +55,14 @@
|
|
|
55
55
|
cursor: default;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/* 올리면 설명 말풍선이 뜨는 자리의 호버 — **아이콘 색으로 1px 테두리**(말귀 (106), 사용자 지시
|
|
59
|
+
2026-10-10). 안쪽 그림자라 40px 이 안 흔들린다. 표는 `--interactive` 에만 산다 —
|
|
60
|
+
`useHover={false}` 거나 보여 줄 글이 없으면 말풍선이 안 서니 테두리도 안 선다. 맨
|
|
61
|
+
`.category-icon__wrap:hover` 로 적지 말 것(그러면 반응 안 하는 자리가 반응하는 척한다). */
|
|
62
|
+
.category-icon__wrap--interactive:hover .category-icon {
|
|
63
|
+
box-shadow: inset 0 0 0 1px var(--t-item-fg, var(--color-lightGray));
|
|
64
|
+
}
|
|
65
|
+
|
|
58
66
|
/* ── Hover tooltip ────────────────────────────────────── */
|
|
59
67
|
.category-icon__tooltip {
|
|
60
68
|
white-space: nowrap;
|
|
@@ -25,6 +25,14 @@ export default function CategoryIcon({ icon, label, tooltip, wrapperStyle, color
|
|
|
25
25
|
};
|
|
26
26
|
}, [wrapperStyle, color, bgColor]);
|
|
27
27
|
const tipText = tooltip || label;
|
|
28
|
+
/**
|
|
29
|
+
* **반응하는 자리면 반응하는 것처럼 보인다**(말귀 요청 (106), 사용자 지시 2026-10-10 —
|
|
30
|
+
* *"category icon 에 color 로 border 1 표현"*). 올리면 설명 말풍선이 뜨는데 상자는 그대로였다.
|
|
31
|
+
* 표는 CSS 의 맨 `:hover` 로 못 준다 — `useHover={false}` 면 말풍선이 안 서는데 테두리만
|
|
32
|
+
* 서면 (105) 의 반대 거짓이다. 그래서 **말풍선이 설 때만** 이 표를 붙이고, 호버 테두리는
|
|
33
|
+
* `--interactive` 에만 산다. 보여 줄 글이 없으면(`tipText` 없음) 올려도 아무 일이 없으니 표도 없다.
|
|
34
|
+
*/
|
|
35
|
+
const 반응하나 = useHover && Boolean(tipText);
|
|
28
36
|
const [anchorRect, setAnchorRect] = useState(null);
|
|
29
37
|
useLayoutEffect(() => {
|
|
30
38
|
if (hover)
|
|
@@ -77,5 +85,5 @@ export default function CategoryIcon({ icon, label, tooltip, wrapperStyle, color
|
|
|
77
85
|
setHover(v => !v);
|
|
78
86
|
}
|
|
79
87
|
}, []);
|
|
80
|
-
return (_jsxs("span", { ref: ref, className:
|
|
88
|
+
return (_jsxs("span", { ref: ref, className: cx('category-icon__wrap', 반응하나 && 'category-icon__wrap--interactive'), onPointerEnter: handleHoverOn, onPointerLeave: handleHoverOff, onFocus: handleHoverOn, onBlur: handleHoverOff, onKeyDown: handleKeyDown, role: "button", tabIndex: 0, "aria-label": typeof tipText === 'string' ? t('common.descriptionFor', { name: tipText, defaultValue: `${tipText} 설명` }) : undefined, "aria-haspopup": !!tipText, "aria-expanded": hover, "data-tooltip-ignore": true, children: [_jsx(Icon, { icon: icon, label: label, className: cls, imgClassName: "category-icon__image", maskClassName: "category-icon__mask", svgClassName: "category-icon__svg", wrapperStyle: resolvedWrapperStyle, size: svgSize }), hover && tipText && (_jsx(InfoTooltip, { anchorRect: anchorRect, text: typeof tipText === 'string' ? tipText : undefined, offset: 8, onClose: handleTooltipClose, className: "category-icon__tooltip", ariaLabel: typeof tipText === 'string' ? tipText : undefined }))] }));
|
|
81
89
|
}
|
|
@@ -14,8 +14,15 @@ export type TrainingStepProps = {
|
|
|
14
14
|
* **왜 아직 못 누르는지 알리는 한 줄** — 잠긴 단계 **바로 다음 차례**에만 선다
|
|
15
15
|
* (말귀 요청 (99), 2026-10-08).
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* **주는 곳에서만 선다 — 안 주면 아무것도 안 선다**(말귀 요청 (107), 사용자 지시 2026-10-10:
|
|
18
|
+
* 실행 창의 *"다 들으면 고를 수 있어요."* 를 보고 *"누가 추가했어, 제거해"*). 0.302.0 ~ 0.309.0
|
|
19
|
+
* 에는 안 줘도 그 말이 기본으로 섰는데, **앱에서는 끌 길이 없었다** — 그 단계는 코어 러너가
|
|
20
|
+
* 세워서 앱이 이 prop 을 만질 자리가 없다(앱 저장소 0건). 켜고 싶으면 `t('training.answerLockedHint')`
|
|
21
|
+
* 를 그대로 주면 된다(열쇠는 남겨 두었다). `null` 은 안 준 것과 같다.
|
|
22
|
+
*
|
|
23
|
+
* (99) 의 낭독기 쪽 까닭(단추가 진짜 `disabled` 라 초점이 안 가 까닭을 들을 길이 없다)은
|
|
24
|
+
* 그대로 참이다 — 주면 여전히 `role="status"` 로 선다. 보이는 글 없이 그 길만 지키려면
|
|
25
|
+
* 부르는 쪽이 시각적으로 숨긴 노드를 주면 된다. 코어가 몰래 세우지는 않는다.
|
|
19
26
|
*/
|
|
20
27
|
lockedHint?: ReactNode | null;
|
|
21
28
|
};
|
|
@@ -87,6 +87,6 @@ export default function TrainingStep({ index, stepIndex, isLast = false, title =
|
|
|
87
87
|
}
|
|
88
88
|
return child;
|
|
89
89
|
});
|
|
90
|
-
return (_jsxs("div", { className: `training-step training-step--${currentState} ${isLast ? "training-step--last" : ""} ${className}`, "aria-current": currentState === 'active', children: [_jsx(TrainingStepBullet, { state: currentState }), _jsxs("div", { className: "training-step__content", children: [_jsx(TrainingStepTitle, { state: currentState, title: title, ...titleProps, useFaded: useTitleFaded }), currentState === 'disabled' && lockedHint
|
|
91
|
-
&& typeof index === 'number' && typeof stepIndex === 'number' && index === stepIndex + 1 && (_jsx("p", { className: "training-step__locked-hint caption-1", role: "status", children: lockedHint
|
|
90
|
+
return (_jsxs("div", { className: `training-step training-step--${currentState} ${isLast ? "training-step--last" : ""} ${className}`, "aria-current": currentState === 'active', children: [_jsx(TrainingStepBullet, { state: currentState }), _jsxs("div", { className: "training-step__content", children: [_jsx(TrainingStepTitle, { state: currentState, title: title, ...titleProps, useFaded: useTitleFaded }), currentState === 'disabled' && lockedHint
|
|
91
|
+
&& typeof index === 'number' && typeof stepIndex === 'number' && index === stepIndex + 1 && (_jsx("p", { className: "training-step__locked-hint caption-1", role: "status", children: lockedHint })), mappedChildren] }), tooltipOpen ? (_jsx(Tooltip, { anchorRect: tooltipOpts.anchorRect, backdrop: tooltipOpts.backdrop, onClose: hideTooltip, ariaLabel: t('tooltip.textbox', '본문'), children: _jsx(TextBoxTooltip, { text: tooltipOpts.text, onClose: hideTooltip }) })) : null] }));
|
|
92
92
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
1
2
|
import './TrainingStep.css';
|
|
2
3
|
import './TrainingStepCard.css';
|
|
3
4
|
/** @public */
|
|
@@ -17,6 +18,8 @@ export type TrainingStepCardProps = {
|
|
|
17
18
|
index?: number;
|
|
18
19
|
stepIndex?: number;
|
|
19
20
|
isLast?: boolean;
|
|
21
|
+
/** 잠긴 까닭 한 줄 — `TrainingStep` 으로 그대로 넘긴다. (107): 안 주면 안 선다. (99) 때는 이 겉싸개가 안 넘겨 맨 `TrainingStep` 에서만 줄 수 있었다 */
|
|
22
|
+
lockedHint?: ReactNode | null;
|
|
20
23
|
title?: string;
|
|
21
24
|
onDone?: (payload?: unknown) => void;
|
|
22
25
|
onChange?: (payload?: unknown) => void;
|
|
@@ -49,4 +52,4 @@ export type TrainingStepCardProps = {
|
|
|
49
52
|
playAllGapMs?: number;
|
|
50
53
|
};
|
|
51
54
|
/** @public */
|
|
52
|
-
export default function TrainingStepCard({ index, stepIndex, isLast, title, onDone, onChange, context, selectable, hideLabel, hideAudio, maxCards, playAllGapMs }: TrainingStepCardProps): import("react").JSX.Element;
|
|
55
|
+
export default function TrainingStepCard({ index, stepIndex, isLast, lockedHint, title, onDone, onChange, context, selectable, hideLabel, hideAudio, maxCards, playAllGapMs }: TrainingStepCardProps): import("react").JSX.Element;
|
|
@@ -21,7 +21,7 @@ function withTimeout(promise, ms) {
|
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
/** @public */
|
|
24
|
-
export default function TrainingStepCard({ index, stepIndex, isLast = false, title, onDone, onChange, context = [], selectable = false, hideLabel = false, hideAudio = false, maxCards, playAllGapMs = 0 }) {
|
|
24
|
+
export default function TrainingStepCard({ index, stepIndex, isLast = false, lockedHint, title, onDone, onChange, context = [], selectable = false, hideLabel = false, hideAudio = false, maxCards, playAllGapMs = 0 }) {
|
|
25
25
|
const { t } = useTranslation();
|
|
26
26
|
const resolvedTitle = title ?? t('training.cardInstructions', '낱말 카드를 보고 소리를 듣고 확인하세요.');
|
|
27
27
|
const items = useMemo(() => Array.isArray(context) ? context : (context && context.items) || [], [context]);
|
|
@@ -133,7 +133,7 @@ export default function TrainingStepCard({ index, stepIndex, isLast = false, tit
|
|
|
133
133
|
catch (e) { }
|
|
134
134
|
}
|
|
135
135
|
}, [onChange, currentState, onDone]);
|
|
136
|
-
return (_jsx(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, title: resolvedTitle, onDone: onDone, titleProps: {
|
|
136
|
+
return (_jsx(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, lockedHint: lockedHint, title: resolvedTitle, onDone: onDone, titleProps: {
|
|
137
137
|
usePlayAllBtn: !hideAudio && shown.length > 0,
|
|
138
138
|
onPlayAll: handlePlayAll,
|
|
139
139
|
playAllActive: playingAll,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
1
2
|
import './TrainingStep.css';
|
|
2
3
|
import './TrainingStepCard.css';
|
|
3
4
|
/** @public */
|
|
@@ -5,6 +6,8 @@ export interface TrainingStepCardOrderProps {
|
|
|
5
6
|
index?: number;
|
|
6
7
|
stepIndex?: number;
|
|
7
8
|
isLast?: boolean;
|
|
9
|
+
/** 잠긴 까닭 한 줄 — `TrainingStep` 으로 그대로 넘긴다. (107): 안 주면 안 선다. (99) 때는 이 겉싸개가 안 넘겨 맨 `TrainingStep` 에서만 줄 수 있었다 */
|
|
10
|
+
lockedHint?: ReactNode | null;
|
|
8
11
|
title?: string;
|
|
9
12
|
/** 카드에 표시할 단어 목록 (섞인 순서) */
|
|
10
13
|
choices?: string[];
|
|
@@ -12,4 +15,4 @@ export interface TrainingStepCardOrderProps {
|
|
|
12
15
|
onChange?: (payload?: unknown) => void;
|
|
13
16
|
}
|
|
14
17
|
/** @public */
|
|
15
|
-
export default function TrainingStepCardOrder({ index, stepIndex, isLast, title, choices, onDone, onChange, }: TrainingStepCardOrderProps): import("react").JSX.Element;
|
|
18
|
+
export default function TrainingStepCardOrder({ index, stepIndex, isLast, lockedHint, title, choices, onDone, onChange, }: TrainingStepCardOrderProps): import("react").JSX.Element;
|
|
@@ -7,7 +7,7 @@ import TrainingWordCard from './TrainingWordCard';
|
|
|
7
7
|
import './TrainingStep.css';
|
|
8
8
|
import './TrainingStepCard.css';
|
|
9
9
|
/** @public */
|
|
10
|
-
export default function TrainingStepCardOrder({ index, stepIndex, isLast = false, title, choices = [], onDone, onChange, }) {
|
|
10
|
+
export default function TrainingStepCardOrder({ index, stepIndex, isLast = false, lockedHint, title, choices = [], onDone, onChange, }) {
|
|
11
11
|
const { t } = useTranslation();
|
|
12
12
|
const idx = typeof index === 'number' ? index : -1;
|
|
13
13
|
const currentState = typeof stepIndex !== 'number'
|
|
@@ -74,5 +74,5 @@ export default function TrainingStepCardOrder({ index, stepIndex, isLast = false
|
|
|
74
74
|
}
|
|
75
75
|
}, [currentState, selectedOrder, nextRank, onChange, onDone, choices]);
|
|
76
76
|
const resolvedTitle = title ?? t('training.selectInOrder', '들은 순서대로 카드를 선택하세요.');
|
|
77
|
-
return (_jsx(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, title: resolvedTitle, onDone: onDone, children: _jsx("div", { className: "training-step__cards", children: choices.map((word, i) => (_jsx(TrainingWordCard, { item: { word }, disabled: currentState === 'disabled', selectable: true, selected: selectedOrder[i] !== null, orderRank: selectedOrder[i], onSelect: () => handleSelect(i), flipDelay: i * 120, hideLabel: false, hideAudio: true }, `${word}-${i}`))) }) }));
|
|
77
|
+
return (_jsx(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, lockedHint: lockedHint, title: resolvedTitle, onDone: onDone, children: _jsx("div", { className: "training-step__cards", children: choices.map((word, i) => (_jsx(TrainingWordCard, { item: { word }, disabled: currentState === 'disabled', selectable: true, selected: selectedOrder[i] !== null, orderRank: selectedOrder[i], onSelect: () => handleSelect(i), flipDelay: i * 120, hideLabel: false, hideAudio: true }, `${word}-${i}`))) }) }));
|
|
78
78
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
1
2
|
import "./TrainingStep.css";
|
|
2
3
|
import type { TrainingPayload } from '../../types/training';
|
|
3
4
|
/** @public */
|
|
@@ -5,6 +6,8 @@ export type TrainingStepConfirmProps = {
|
|
|
5
6
|
index?: number;
|
|
6
7
|
stepIndex?: number;
|
|
7
8
|
isLast?: boolean;
|
|
9
|
+
/** 잠긴 까닭 한 줄 — `TrainingStep` 으로 그대로 넘긴다. (107): 안 주면 안 선다. (99) 때는 이 겉싸개가 안 넘겨 맨 `TrainingStep` 에서만 줄 수 있었다 */
|
|
10
|
+
lockedHint?: ReactNode | null;
|
|
8
11
|
title?: string;
|
|
9
12
|
useTitleFaded?: boolean;
|
|
10
13
|
onDone?: (payload?: TrainingPayload) => void;
|
|
@@ -31,4 +34,4 @@ export type TrainingStepConfirmProps = {
|
|
|
31
34
|
shuffleChoices?: boolean;
|
|
32
35
|
};
|
|
33
36
|
/** @public */
|
|
34
|
-
export default function TrainingStepConfirm({ index, stepIndex, isLast, title, useTitleFaded, onDone, onChange, answer, wrongAnswers, choices: propChoices, multiSelect, correctCount, useDisabledCovered, choiceIcons, shuffleChoices }: TrainingStepConfirmProps): import("react").JSX.Element;
|
|
37
|
+
export default function TrainingStepConfirm({ index, stepIndex, isLast, lockedHint, title, useTitleFaded, onDone, onChange, answer, wrongAnswers, choices: propChoices, multiSelect, correctCount, useDisabledCovered, choiceIcons, shuffleChoices }: TrainingStepConfirmProps): import("react").JSX.Element;
|
|
@@ -7,7 +7,7 @@ import "./TrainingStep.css";
|
|
|
7
7
|
import TrainingStep from "./TrainingStep";
|
|
8
8
|
import { shuffle as shuffleArray } from '../../utils/array';
|
|
9
9
|
/** @public */
|
|
10
|
-
export default function TrainingStepConfirm({ index, stepIndex, isLast = false, title, useTitleFaded, onDone, onChange, answer, wrongAnswers, choices: propChoices, multiSelect = false, correctCount = 0, useDisabledCovered = false, choiceIcons, shuffleChoices = true }) {
|
|
10
|
+
export default function TrainingStepConfirm({ index, stepIndex, isLast = false, lockedHint, title, useTitleFaded, onDone, onChange, answer, wrongAnswers, choices: propChoices, multiSelect = false, correctCount = 0, useDisabledCovered = false, choiceIcons, shuffleChoices = true }) {
|
|
11
11
|
const { t } = useTranslation();
|
|
12
12
|
const resolvedTitle = title ?? t('training.selectConfirm', '문제를 보고 해당하는 내용을 선택하세요.');
|
|
13
13
|
const idx = typeof index === 'number' ? index : -1;
|
|
@@ -123,5 +123,5 @@ export default function TrainingStepConfirm({ index, stepIndex, isLast = false,
|
|
|
123
123
|
때문이다 — 나머지 둘(choices 바뀜·페이지 전환)은 빈 Set 으로 되돌리는 자리라
|
|
124
124
|
저 effect 의 `size === 0` 가드에 걸려 어차피 아무것도 안 했다. */
|
|
125
125
|
const btnsClassName = `training-step__buttons${Array.isArray(choices) && choices.length > 3 ? ' training-step__buttons--wrap-3' : ''}`;
|
|
126
|
-
return (_jsxs(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, title: resolvedTitle, useTitleFaded: useTitleFaded, children: [_jsx("div", { className: btnsClassName, children: Array.isArray(choices) && choices.length > 0 ? (choices.map((c, i) => (_jsx(Button, { htmlType: "button", size: 56, type: "bar", onClick: () => handleSelect(i), ariaLabel: String(c), label: c, useLabel: true, icon: choiceIcons ? choiceIcons[String(c)] : undefined, variant: 'default', disabled: currentState === 'disabled', selected: multiSelect ? selectedSet.has(c) : selectedAnswer === c, useDisabledCovered: useDisabledCovered, minWidth: 0 }, String(c))))) : null }), multiSelect && (_jsx("div", { className: "training-step__buttons", children: _jsx(Button, { htmlType: "button", size: 56, type: "bar", variant: "main", onClick: handleMultiSubmit, disabled: currentState !== 'active' || selectedSet.size === 0, ariaLabel: t('common.confirm', '확인'), label: t('common.confirm', '확인'), useLabel: true, minWidth: 0 }) }))] }));
|
|
126
|
+
return (_jsxs(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, lockedHint: lockedHint, title: resolvedTitle, useTitleFaded: useTitleFaded, children: [_jsx("div", { className: btnsClassName, children: Array.isArray(choices) && choices.length > 0 ? (choices.map((c, i) => (_jsx(Button, { htmlType: "button", size: 56, type: "bar", onClick: () => handleSelect(i), ariaLabel: String(c), label: c, useLabel: true, icon: choiceIcons ? choiceIcons[String(c)] : undefined, variant: 'default', disabled: currentState === 'disabled', selected: multiSelect ? selectedSet.has(c) : selectedAnswer === c, useDisabledCovered: useDisabledCovered, minWidth: 0 }, String(c))))) : null }), multiSelect && (_jsx("div", { className: "training-step__buttons", children: _jsx(Button, { htmlType: "button", size: 56, type: "bar", variant: "main", onClick: handleMultiSubmit, disabled: currentState !== 'active' || selectedSet.size === 0, ariaLabel: t('common.confirm', '확인'), label: t('common.confirm', '확인'), useLabel: true, minWidth: 0 }) }))] }));
|
|
127
127
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
1
2
|
import "./TrainingStep.css";
|
|
2
3
|
import type { TrainingPayload } from '../../types/training';
|
|
3
4
|
/** @public */
|
|
@@ -5,6 +6,8 @@ export type TrainingStepConfirmWithTextProps = {
|
|
|
5
6
|
index?: number;
|
|
6
7
|
stepIndex?: number;
|
|
7
8
|
isLast?: boolean;
|
|
9
|
+
/** 잠긴 까닭 한 줄 — `TrainingStep` 으로 그대로 넘긴다. (107): 안 주면 안 선다. (99) 때는 이 겉싸개가 안 넘겨 맨 `TrainingStep` 에서만 줄 수 있었다 */
|
|
10
|
+
lockedHint?: ReactNode | null;
|
|
8
11
|
title?: string;
|
|
9
12
|
onDone?: (payload?: TrainingPayload) => void;
|
|
10
13
|
onChange?: (payload?: TrainingPayload) => void;
|
|
@@ -13,4 +16,4 @@ export type TrainingStepConfirmWithTextProps = {
|
|
|
13
16
|
demoSelected?: number | boolean | null;
|
|
14
17
|
};
|
|
15
18
|
/** @public */
|
|
16
|
-
export default function TrainingStepConfirmWithText({ index, stepIndex, isLast, title, onDone, onChange, context, demoSelected }: TrainingStepConfirmWithTextProps): import("react").JSX.Element;
|
|
19
|
+
export default function TrainingStepConfirmWithText({ index, stepIndex, isLast, lockedHint, title, onDone, onChange, context, demoSelected }: TrainingStepConfirmWithTextProps): import("react").JSX.Element;
|
|
@@ -7,7 +7,7 @@ import "./TrainingStep.css";
|
|
|
7
7
|
import TrainingStep from "./TrainingStep";
|
|
8
8
|
import TrainingStepTextBox from "./TrainingStepTextBox";
|
|
9
9
|
/** @public */
|
|
10
|
-
export default function TrainingStepConfirmWithText({ index, stepIndex, isLast = false, title, onDone, onChange, context, demoSelected }) {
|
|
10
|
+
export default function TrainingStepConfirmWithText({ index, stepIndex, isLast = false, lockedHint, title, onDone, onChange, context, demoSelected }) {
|
|
11
11
|
const { t } = useTranslation();
|
|
12
12
|
const resolvedTitle = title ?? t('training.confirmTitle', '문장을 확인하고 잘 들렸는지 선택하세요.');
|
|
13
13
|
const idx = typeof index === 'number' ? index : -1;
|
|
@@ -70,7 +70,7 @@ export default function TrainingStepConfirmWithText({ index, stepIndex, isLast =
|
|
|
70
70
|
setSelectedAnswer(true);
|
|
71
71
|
handleEndedLocal(true);
|
|
72
72
|
}, [handleEndedLocal, currentState]);
|
|
73
|
-
return (_jsxs(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, title: resolvedTitle, onDone: onDone, useTitleFaded: false, children: [_jsx(TrainingStepTextBox, { context: context, state: currentState }), _jsx("div", { className: "training-step__buttons", children: Array.isArray(choices) && choices.length > 0 ? (choices.slice(0, 6).map((c, i) => (_jsx(Button, { htmlType: "button", size: 56, type: "bar", onClick: () => {
|
|
73
|
+
return (_jsxs(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, lockedHint: lockedHint, title: resolvedTitle, onDone: onDone, useTitleFaded: false, children: [_jsx(TrainingStepTextBox, { context: context, state: currentState }), _jsx("div", { className: "training-step__buttons", children: Array.isArray(choices) && choices.length > 0 ? (choices.slice(0, 6).map((c, i) => (_jsx(Button, { htmlType: "button", size: 56, type: "bar", onClick: () => {
|
|
74
74
|
if (currentState !== 'active')
|
|
75
75
|
return;
|
|
76
76
|
setSelectedAnswer(i);
|
|
@@ -3,4 +3,4 @@ import "./TrainingStep.css";
|
|
|
3
3
|
import "./TrainingStepOrderList.css";
|
|
4
4
|
import type { TrainingStepOrderListProps } from './trainingStepOrderListTypes';
|
|
5
5
|
export type { TrainingContextItem, ShuffledItem, ChangeResult, TrainingStepOrderListProps } from './trainingStepOrderListTypes';
|
|
6
|
-
export default function TrainingStepOrderList({ index, stepIndex, isLast, title, onDone, onChange, context, }: TrainingStepOrderListProps): import("react").JSX.Element;
|
|
6
|
+
export default function TrainingStepOrderList({ index, stepIndex, isLast, lockedHint, title, onDone, onChange, context, }: TrainingStepOrderListProps): import("react").JSX.Element;
|
|
@@ -12,7 +12,7 @@ import "./TrainingStepOrderList.css";
|
|
|
12
12
|
/* 기본 context 는 **모듈에 하나** — 매 렌더 새 `[]` 면 [context] effect 가 매번 다시 돌아 setState·onChange 로 렌더가 끝없이
|
|
13
13
|
반복됐다(자료를 불러오기 전 context 없이 그린 단계에서 CPU 100%). */
|
|
14
14
|
const 빈문맥 = [];
|
|
15
|
-
export default function TrainingStepOrderList({ index, stepIndex, isLast = false, title, onDone, onChange, context = 빈문맥, }) {
|
|
15
|
+
export default function TrainingStepOrderList({ index, stepIndex, isLast = false, lockedHint, title, onDone, onChange, context = 빈문맥, }) {
|
|
16
16
|
const { t } = useTranslation();
|
|
17
17
|
const resolvedTitle = title ?? t('training.orderListDefault', '문장을 들은 순서대로 배치하세요.');
|
|
18
18
|
const toText = (v) => typeof v === "string"
|
|
@@ -459,7 +459,7 @@ export default function TrainingStepOrderList({ index, stepIndex, isLast = false
|
|
|
459
459
|
}
|
|
460
460
|
};
|
|
461
461
|
}, []);
|
|
462
|
-
return (_jsx(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, title: resolvedTitle, children: _jsx("div", { className: "order-list", children: shuffled.map((it, idx) => {
|
|
462
|
+
return (_jsx(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, lockedHint: lockedHint, title: resolvedTitle, children: _jsx("div", { className: "order-list", children: shuffled.map((it, idx) => {
|
|
463
463
|
const total = Math.max(1, (shuffled || []).length);
|
|
464
464
|
const pct = total > 1 ? Math.round((idx / (total - 1)) * 100) : 50;
|
|
465
465
|
return (_jsx("div", { className: `order-list__item`, children: _jsxs("div", { className: "order-list__slot", "data-disabled": currentState === 'disabled', "aria-disabled": currentState === 'disabled', children: [currentState !== 'disabled' ? (_jsx("div", { className: "order-list__marker caption-1", style: { '--marker-pos': pct + '%' }, ref: (el) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
1
2
|
/**
|
|
2
3
|
* trainingStepOrderListTypes.ts
|
|
3
4
|
* TrainingStepOrderList의 순수 타입 정의.
|
|
@@ -27,6 +28,8 @@ export interface TrainingStepOrderListProps {
|
|
|
27
28
|
index?: number;
|
|
28
29
|
stepIndex?: number;
|
|
29
30
|
isLast?: boolean;
|
|
31
|
+
/** 잠긴 까닭 한 줄 — `TrainingStep` 으로 그대로 넘긴다. (107): 안 주면 안 선다. (99) 때는 이 겉싸개가 안 넘겨 맨 `TrainingStep` 에서만 줄 수 있었다 */
|
|
32
|
+
lockedHint?: ReactNode | null;
|
|
30
33
|
title?: string;
|
|
31
34
|
onDone?: (res: ChangeResult) => void;
|
|
32
35
|
onChange?: (res: ChangeResult) => void;
|
|
@@ -36,7 +36,16 @@ load) {
|
|
|
36
36
|
return mod;
|
|
37
37
|
}
|
|
38
38
|
/* 여기 오는 길은 사실상 하나뿐이다 — 청크를 못 받아 복구가 도는 중. 새 판을
|
|
39
|
-
기다린다. 영영 resolve 하지 않는 약속을 돌려 React 가 이 경계를 건드리지 않게 한다.
|
|
39
|
+
기다린다. 영영 resolve 하지 않는 약속을 돌려 React 가 이 경계를 건드리지 않게 한다.
|
|
40
|
+
|
|
41
|
+
**`load()` 가 거절되는 길은 여기 안 온다 — 일부러다.** 위 `await` 가 그대로 던진다.
|
|
42
|
+
막이(10초) 안의 두 번째 실패가 그 길이다(새 index 의 조각도 아직 CDN 에 안 퍼진 때 —
|
|
43
|
+
말귀 prod 2026-10-02, 8초 안에 두 번). 그때 preventDefault 가 안 되어 import 가 거절되는데,
|
|
44
|
+
여기서 `skewReloadPending()` 을 보고 영원한 약속을 돌려주면 **막이가 풀려도 영영 빈 화면**
|
|
45
|
+
이다 — 새로고침은 이미 다 썼고 다시 올 것이 없다. 던져서 경계까지 가야 한다.
|
|
46
|
+
그리고 React `lazy` 는 한 번 거절되면 **굳는다**(`_status = 2`) — 경계의 "다시 시도" 가
|
|
47
|
+
같은 거절을 다시 받는다. 그 오류를 받은 경계는 **새로고침을 쥐어야** 한다(CONSUMING.md
|
|
48
|
+
§2-0-6). 둘 다 `test/chunkSkewLazyPage0917.test.tsx` 의 못이다. */
|
|
40
49
|
if (skewReloadPending())
|
|
41
50
|
return new Promise(() => { });
|
|
42
51
|
throw new Error(`[lazyPage] ${name}: 화면 모듈을 받지 못했습니다(default 내보내기 없음). ` +
|