@ihabdevteam/core 0.299.0 → 0.301.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 +66 -0
- package/dist/components.cjs.js +176 -176
- package/dist/hooks.cjs.js +2 -2
- package/dist/i18n.cjs.js +2 -2
- package/dist/ihabdevteam-core.cjs.js +59 -59
- package/dist/ihabdevteam-core.d.ts +31 -1
- package/dist/packages/core/src/components/Accordion.js +1 -1
- package/dist/packages/core/src/components/FilterPanel.js +6 -6
- package/dist/packages/core/src/components/Table.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingCardGroup.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingPagePopup.d.ts +15 -0
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +64 -24
- package/dist/packages/core/src/components/Training/trainingBlockButtonTypes.d.ts +15 -0
- package/dist/packages/core/src/i18n/locales/ko.json +3 -3
- 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.301.0";
|
|
1655
1655
|
|
|
1656
1656
|
/** @public */
|
|
1657
1657
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -8264,6 +8264,21 @@ export declare interface TrainingBlockButtonPopupProps {
|
|
|
8264
8264
|
correct: number | null;
|
|
8265
8265
|
total: number | null;
|
|
8266
8266
|
}) => void;
|
|
8267
|
+
/**
|
|
8268
|
+
* **판이 끝난 그 순간** 호출 — 닫기와 무관하다(`TrainingPagePopup` 으로 그대로 전달).
|
|
8269
|
+
*
|
|
8270
|
+
* `onComplete` 는 **닫을 때** 불린다. 숙제처럼 *"이 문항 풀었다"* 를 세는 쪽은 닫기를
|
|
8271
|
+
* 기다릴 수 없다 — 다 풀고 안 닫고 떠나면 통째로 날아갔다(말귀 요청 (100), 0.300.0).
|
|
8272
|
+
*
|
|
8273
|
+
* **값은 `...rest` 로 흘러 이미 갔는데 타입만 빠져 있었다**(0.300.0). 쓰는 쪽은 통과시킬
|
|
8274
|
+
* 길이 없어 `as` 로 덮어야 했다 — (74) 가 앱까지 안 닿았던 것과 **같은 꼴**이다:
|
|
8275
|
+
* 가운데 한 겹이 안 따라갔다.
|
|
8276
|
+
*/
|
|
8277
|
+
onRoundFinished?: (result: {
|
|
8278
|
+
correct: number | null;
|
|
8279
|
+
total: number | null;
|
|
8280
|
+
attempt?: number;
|
|
8281
|
+
}) => void;
|
|
8267
8282
|
popupTitle: string;
|
|
8268
8283
|
popupSubTitle?: string;
|
|
8269
8284
|
depth?: string;
|
|
@@ -8735,6 +8750,21 @@ export declare type TrainingPagePopupProps = {
|
|
|
8735
8750
|
/** 완료 시 호출. 결과 팝업이 계산한 정답/전체 문항 수를 함께 전달(호출부가 무시해도 무방하도록 옵셔널 인자).
|
|
8736
8751
|
* `attempt` 는 이 창에서 몇 번째 판인지(1부터, 다시하기마다 +1), `previousAttempts` 는 다시하기 전 판들의 정답/전체 — 전에는 1회차 결과가 어디에도 남지 않았다. */
|
|
8737
8752
|
onComplete?: (result?: TrainingCompleteResult) => void;
|
|
8753
|
+
/**
|
|
8754
|
+
* **판이 끝난 그 순간** 부른다 — 결과창이 서면서, **닫기를 기다리지 않는다**(말귀 요청 (100)).
|
|
8755
|
+
*
|
|
8756
|
+
* `onComplete` 는 **닫을 때** 불린다. 그래서 다 풀고 **안 닫고 떠나면**(뒤로 가기 · 새로고침)
|
|
8757
|
+
* 아무것도 안 남았다 — 화면은 이미 `5/5` 라고 말한 뒤인데 숙제 진행률이 통째로 날아갔다
|
|
8758
|
+
* (말귀 실측 2026-10-08: 뒤로 가기·새로고침 둘 다 `rpc` 0건).
|
|
8759
|
+
*
|
|
8760
|
+
* **판이 끝났다는 것은 사실이고, 결과창을 어떻게 닫았다는 것은 조작이다.** 조작이 사실을
|
|
8761
|
+
* 정하면 안 된다 — 닫는 길이 넷인데 둘이 안 적었고, 새 길이 하나 더 생기면 또 빠진다.
|
|
8762
|
+
*
|
|
8763
|
+
* 숙제처럼 **"이 문항 풀었다" 를 세는 쪽은 이 손잡이**를 쓴다. `onComplete` 는 그대로
|
|
8764
|
+
* 닫을 때 불리므로, 닫으면서 할 일(창 닫기 · 다음으로)은 거기 두면 된다.
|
|
8765
|
+
* 한 판에 **한 번만** 불린다(다시하기를 하면 새 판으로 또 한 번).
|
|
8766
|
+
*/
|
|
8767
|
+
onRoundFinished?: (result: TrainingCompleteResult) => void;
|
|
8738
8768
|
onNext?: () => void;
|
|
8739
8769
|
onSettings?: () => void;
|
|
8740
8770
|
/** 좌측 하단 훈련 설정 버튼 + 오디오 미터 토글 표시 여부. 기본 true — 모든 훈련에서 노출되며,
|
|
@@ -30,7 +30,7 @@ function Item({ title, body, open = false, onToggle, avatar, titleClassName = 'b
|
|
|
30
30
|
/* **접기 손잡이의 말은 저장소 전체가 하나다**(사용자 지시 "전체 통일해", 2026-10-07).
|
|
31
31
|
열쇠는 `accordion.*` 로 따로 남는다(소비자가 아코디언만 달리 부르고 싶을 수 있다) —
|
|
32
32
|
대신 **값이 갈라지지 않는지**를 못이 센다(`filterPanelCollapseAffordance`). */
|
|
33
|
-
label: open ? t('accordion.collapse', '접기') : t('accordion.expand', '
|
|
33
|
+
label: open ? t('accordion.collapse', '접기') : t('accordion.expand', '펼치기'), className: "accordion-item__chevron-icon", imgClassName: "accordion-item__chevron-image" }) })] }), open && (_jsx("div", { className: `accordion-item__body ${bodyClassName}`, children: typeof body === 'string' ? _jsx("p", { children: body }) : body }))] }));
|
|
34
34
|
}
|
|
35
35
|
// ── Accordion ─────────────────────────────────────────────────────────────
|
|
36
36
|
/** @public */
|
|
@@ -73,12 +73,12 @@ export default function FilterPanel({ title, titleLevel = 2, headerAction, heade
|
|
|
73
73
|
그리고 `common.close`(대화상자 닫기)와 **섞지 않는다** — 섞으면 접기 손잡이와
|
|
74
74
|
닫기 단추가 한 결정에 묶여, 한쪽을 고칠 때 다른 쪽이 따라 움직인다.
|
|
75
75
|
|
|
76
|
-
>
|
|
77
|
-
>
|
|
78
|
-
>
|
|
79
|
-
>
|
|
80
|
-
>
|
|
81
|
-
ariaLabel: collapsed ? t('common.expand', '
|
|
76
|
+
> **짝은 `펼치기`/`접기` 다**(사용자 결정 2026-10-08). 하루 사이에 말이
|
|
77
|
+
> 두 번 움직였다 — `펼치기/접기`(처음) → `열기/닫기`(10-07) → `열기/접기`
|
|
78
|
+
> (10-08, `닫기` 만 고침) → **`펼치기/접기`**(10-08, 짝을 맞춤).
|
|
79
|
+
> 섞인 하루를 **제 짐작으로 안 맞춘 것**이 맞았다: 두 말 다 사장님이
|
|
80
|
+
> 고르신 것이라, 어느 쪽으로 모을지도 사장님이 정하실 일이었다. */
|
|
81
|
+
ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), showTooltipInfo: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), "aria-expanded": !collapsed, onClick: toggleCollapsed }))] })] })), !collapsed && (_jsxs(_Fragment, { children: [_jsx("div", { className: cx('filter-panel__sections', isRow ? 'filter-panel__sections--row' : ''), children: sections.map(section => {
|
|
82
82
|
const sel = selected[section.label];
|
|
83
83
|
const isAllSelected = sel === undefined || (Array.isArray(sel) ? sel.length === 0 : false);
|
|
84
84
|
// section.items에 빈 항목(null/undefined)이 섞여 들어오면 item.value 접근에서
|
|
@@ -528,7 +528,7 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
528
528
|
* 손으로 붙이던 `onKeyDown` 은 **지웠다** — 진짜 단추라 Enter·Space 는 브라우저가
|
|
529
529
|
* 처리하고, "머리 안 단추에서 누른 키는 그 단추 몫" 이라는 가드도 저절로 선다.
|
|
530
530
|
*/
|
|
531
|
-
role: "caption", children: [onToggleCollapse && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-right' : 'chevron-down', ariaLabel: collapsed ? t('common.expand', '
|
|
531
|
+
role: "caption", children: [onToggleCollapse && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-right' : 'chevron-down', ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'),
|
|
532
532
|
/* 여닫힘은 **여는 것**이 말한다 — 전에는 겉의 `div` 에 달려 있었다(위 머리글) */
|
|
533
533
|
ariaExpanded: !collapsed, className: "table-header__chevron" })), icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "table-header__icon" })), _jsx("span", { id: 제목id, className: "table-header__title h5b", children: title }), count !== undefined && _jsx("span", { className: "table-header__count body-3b", children: count }), headerActions && (_jsx("span", { className: "table-header__actions", onClick: e => e.stopPropagation(), children: headerActions }))] })), !collapsed && (_jsxs(_Fragment, { children: [headerGroups && headerGroups.length > 0 && (_jsx("div", { role: "row", className: "table-fields table-fields--groups", children: headerGroups.map((g, i) => (_jsx("div", { role: "columnheader", "aria-colspan": g.span, className: cx('table-field body-3', g.className), style: { gridColumn: `span ${g.span}` }, children: g.label }, i))) })), _jsxs("div", { role: "presentation", className: cx('table-fields-strip', 폭조절 && 'table-fields-strip--resizable'), children: [_jsx("div", { role: "row", className: "table-fields", ref: fieldsRef, children: fields.map((f, i) => (_jsx(TableField, { onClick: f.onClick, className: f.className, sortDir: f.sortDir, ariaSort: ariaSortOf(i),
|
|
534
534
|
/* fit 모드는 오른쪽 이웃에서 폭을 덜어 오므로, 덜어 올 컬럼이 없는 자리에는
|
|
@@ -25,7 +25,7 @@ export default function TrainingCardGroup({ title, titleLevel = 3, count, collap
|
|
|
25
25
|
때만 — 마디 제목이면 이름을 못 짓는다. */
|
|
26
26
|
ariaLabel: typeof title === 'string' && title
|
|
27
27
|
? (collapsed ? t('common.expandFor', { name: title }) : t('common.collapseFor', { name: title }))
|
|
28
|
-
: (collapsed ? t('common.expand', '
|
|
28
|
+
: (collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기')), className: "training-card-group__chevron", "aria-expanded": !collapsed, onClick: onToggle })), typeof title === 'string'
|
|
29
29
|
? _jsx(제목태그, { id: 제목id, className: "training-card-group__title h5b", children: title })
|
|
30
30
|
: title, count !== undefined && _jsx("span", { className: "training-card-group__count body-3b", children: count }), (sort || actions != null) && (_jsxs("span", { className: "training-card-group__actions", onClick: e => e.stopPropagation(), children: [sort && (_jsx(Dropdown, { size: 40, options: sort.options, value: sort.value, placeholder: sort.placeholder, onChange: sort.onChange,
|
|
31
31
|
/* 접기와 같은 까닭으로 묶음 이름을 단다 — 그룹마다 `정렬` 이 하나씩 선다 */
|
|
@@ -54,6 +54,21 @@ export type TrainingPagePopupProps = {
|
|
|
54
54
|
/** 완료 시 호출. 결과 팝업이 계산한 정답/전체 문항 수를 함께 전달(호출부가 무시해도 무방하도록 옵셔널 인자).
|
|
55
55
|
* `attempt` 는 이 창에서 몇 번째 판인지(1부터, 다시하기마다 +1), `previousAttempts` 는 다시하기 전 판들의 정답/전체 — 전에는 1회차 결과가 어디에도 남지 않았다. */
|
|
56
56
|
onComplete?: (result?: TrainingCompleteResult) => void;
|
|
57
|
+
/**
|
|
58
|
+
* **판이 끝난 그 순간** 부른다 — 결과창이 서면서, **닫기를 기다리지 않는다**(말귀 요청 (100)).
|
|
59
|
+
*
|
|
60
|
+
* `onComplete` 는 **닫을 때** 불린다. 그래서 다 풀고 **안 닫고 떠나면**(뒤로 가기 · 새로고침)
|
|
61
|
+
* 아무것도 안 남았다 — 화면은 이미 `5/5` 라고 말한 뒤인데 숙제 진행률이 통째로 날아갔다
|
|
62
|
+
* (말귀 실측 2026-10-08: 뒤로 가기·새로고침 둘 다 `rpc` 0건).
|
|
63
|
+
*
|
|
64
|
+
* **판이 끝났다는 것은 사실이고, 결과창을 어떻게 닫았다는 것은 조작이다.** 조작이 사실을
|
|
65
|
+
* 정하면 안 된다 — 닫는 길이 넷인데 둘이 안 적었고, 새 길이 하나 더 생기면 또 빠진다.
|
|
66
|
+
*
|
|
67
|
+
* 숙제처럼 **"이 문항 풀었다" 를 세는 쪽은 이 손잡이**를 쓴다. `onComplete` 는 그대로
|
|
68
|
+
* 닫을 때 불리므로, 닫으면서 할 일(창 닫기 · 다음으로)은 거기 두면 된다.
|
|
69
|
+
* 한 판에 **한 번만** 불린다(다시하기를 하면 새 판으로 또 한 번).
|
|
70
|
+
*/
|
|
71
|
+
onRoundFinished?: (result: TrainingCompleteResult) => void;
|
|
57
72
|
onNext?: () => void;
|
|
58
73
|
onSettings?: () => void;
|
|
59
74
|
/** 좌측 하단 훈련 설정 버튼 + 오디오 미터 토글 표시 여부. 기본 true — 모든 훈련에서 노출되며,
|
|
@@ -57,7 +57,7 @@ function 판결과셈(resultOverlay) {
|
|
|
57
57
|
const defaultRenderResult = (p) => _jsx(TrainingResultPopup, { score: p.score, contextsLength: p.contextsLength, results: p.results, onReplay: p.onReplay, onClose: p.onClose, pending: p.pending });
|
|
58
58
|
import { getScenarioPageId, getScenarioStepKey, createScenarioFlowState, resolveScenarioStepProps, } from './trainingScenario';
|
|
59
59
|
function TrainingPagePopupInner(props) {
|
|
60
|
-
const { scenario, title, subTitle, depth, icon, wrapperStyle, iconTooltip, pageIndex, pageCount, stepIndex, stepCount, onBack, onComplete, onNext, onSettings, useNoiseSetting = true, children, ariaLabel, label, buildResults, onResult: externalOnResultProp, typeKey, entryPoint, dailyTotal, itemRef, renderResultPopup, noiseOverride, } = props;
|
|
60
|
+
const { scenario, title, subTitle, depth, icon, wrapperStyle, iconTooltip, pageIndex, pageCount, stepIndex, stepCount, onBack, onComplete, onRoundFinished, onNext, onSettings, useNoiseSetting = true, children, ariaLabel, label, buildResults, onResult: externalOnResultProp, typeKey, entryPoint, dailyTotal, itemRef, renderResultPopup, noiseOverride, } = props;
|
|
61
61
|
// 시간 기록 — 문장 4종은 각 래퍼(useTrainingSessionLog)에서 기록하므로 제외, 그 외(단어 등) 공통 기록
|
|
62
62
|
useTrainingSessionLog(typeKey && !SENTENCE_TIME_LOGGED_TYPES.has(typeKey) ? typeKey : '');
|
|
63
63
|
// 팝업 진입 시각 — 완료 정본의 소요 시간 계산 (마운트 시 1회만 effect에서 초기화)
|
|
@@ -913,29 +913,69 @@ function TrainingPagePopupInner(props) {
|
|
|
913
913
|
return cloneElement(el, { ...el.props, _stepIndex: index, _totalSteps: compoundChildrenSteps.length, _stepKey: `step-${index}`, _pageIndex: effectivePageIndex });
|
|
914
914
|
}) }, `replay-${replayCount}`), currentChildrenPage?.pageBottom ? (_jsx("div", { className: "training-page-bottom", children: currentChildrenPage.pageBottom })) : null] }))
|
|
915
915
|
: null;
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
916
|
+
/**
|
|
917
|
+
* **판이 끝났다는 사실을 적는다** — 결과창이 **서는 순간** 한 번(말귀 요청 (100), 2026-10-08).
|
|
918
|
+
*
|
|
919
|
+
* 전에는 이 일이 `handleResultDone`(**결과창을 닫는 동작**)에만 걸려 있었다. 그래서 다 풀고
|
|
920
|
+
* **안 닫고 떠나면** 아무것도 안 남았다 — 뒤로 가기도, 새로고침도(말귀 실측: `rpc` 0건).
|
|
921
|
+
* 화면은 이미 `5/5` 라고 말한 뒤였다.
|
|
922
|
+
*
|
|
923
|
+
* **판이 끝났다는 것은 사실이고, 결과창을 어떻게 닫았다는 것은 조작이다.** 조작이 사실을
|
|
924
|
+
* 정하면 닫는 길마다 같은 일을 적어야 하고, 넷 중 둘이 빠졌듯 **새 길이 생기면 또 빠진다.**
|
|
925
|
+
*
|
|
926
|
+
* 세대로 **판마다 한 번**만 적는다(다시하기는 새 판이라 또 적는다).
|
|
927
|
+
*/
|
|
928
|
+
const 적은세대Ref = useRef(-1);
|
|
929
|
+
const 판끝남적기 = useCallback(() => {
|
|
930
|
+
if (!typeKey)
|
|
931
|
+
return undefined;
|
|
932
|
+
const ro = resultOverlay;
|
|
933
|
+
const { results, correct: finalCorrect, total } = 판결과셈(resultOverlay);
|
|
934
|
+
const 결과 = { correct: finalCorrect, total, attempt: 앞판들Ref.current.length + 1, previousAttempts: [...앞판들Ref.current] };
|
|
935
|
+
/* 이미 이 판을 적었으면 **값만 돌려준다** — 닫을 때 `onComplete` 가 그 값을 받아야 한다 */
|
|
936
|
+
if (적은세대Ref.current === 결과세대Ref.current)
|
|
937
|
+
return 결과;
|
|
938
|
+
적은세대Ref.current = 결과세대Ref.current;
|
|
939
|
+
/* 문항 하나하나를 함께 남긴다 — 여기서 접으면 어느 낱말을 틀렸는지 되찾을 길이 없다.
|
|
940
|
+
'음소별 오류'(08-26 회의)가 이걸 원재료로 쓴다. */
|
|
941
|
+
logTrainingCompletion({
|
|
942
|
+
items: toItemResults(results),
|
|
943
|
+
category: SENTENCE_TIME_LOGGED_TYPES.has(typeKey) ? 'sentence' : 'word',
|
|
944
|
+
trainingType: typeKey,
|
|
945
|
+
entryPoint: entryPoint ?? 'sentence',
|
|
946
|
+
itemRef: itemRef ?? null,
|
|
947
|
+
correct: finalCorrect,
|
|
948
|
+
total,
|
|
949
|
+
score: typeof ro?.score === 'number' ? ro.score : null,
|
|
950
|
+
durationSeconds: Math.max(0, Math.round((Date.now() - completionStartRef.current) / 1000)),
|
|
951
|
+
dailyTotal: dailyTotal ?? null,
|
|
952
|
+
});
|
|
953
|
+
try {
|
|
954
|
+
onRoundFinished?.(결과);
|
|
938
955
|
}
|
|
956
|
+
catch (e) { /* 알리는 쪽의 실패로 적는 일을 막지 않는다 */ }
|
|
957
|
+
return 결과;
|
|
958
|
+
}, [typeKey, entryPoint, dailyTotal, itemRef, resultOverlay, onRoundFinished]);
|
|
959
|
+
/* **결과창이 서면 적는다** — 닫기를 안 기다린다.
|
|
960
|
+
결과를 세우는 자리가 넷이라(갈래·어린이·자식 손잡이·바깥 손잡이) 그 넷에 각각 적으면
|
|
961
|
+
**한 자리가 안 따라간다.** 세운 값을 보고 여기 한 곳에서 적는다.
|
|
962
|
+
`결과계산중`·빈 결과는 아직 사실이 아니다 — 적지 않는다. */
|
|
963
|
+
useEffect(() => {
|
|
964
|
+
if (!resultOverlay || resultOverlay === 결과계산중)
|
|
965
|
+
return;
|
|
966
|
+
if (resultOverlay?.isEmpty)
|
|
967
|
+
return;
|
|
968
|
+
/* **셀 것이 있어야 적는다.** 결과가 `{}` 로 서는 길이 있다(자식이 빈손을 주거나 늦게
|
|
969
|
+
실패했을 때) — 그것을 판이 끝난 것으로 적으면 `0/0` 짜리 기록이 쌓인다.
|
|
970
|
+
그 꼴에서도 **닫을 때는 전과 같이 적힌다**(아래 그물) — 여기서 앞당기지만 않는다. */
|
|
971
|
+
if (!((판결과셈(resultOverlay).total ?? 0) > 0))
|
|
972
|
+
return;
|
|
973
|
+
판끝남적기();
|
|
974
|
+
}, [resultOverlay, 판끝남적기]);
|
|
975
|
+
const handleResultDone = useCallback(() => {
|
|
976
|
+
/* 여기서도 한 번 부른다 — **그물이다.** 위 효과가 못 돈 꼴(결과가 빈 채로 닫는 길 등)에서도
|
|
977
|
+
적히게 두되, 이미 적었으면 세대가 막아 **두 번 안 적힌다.** */
|
|
978
|
+
const completeResult = 판끝남적기();
|
|
939
979
|
결과창닫기();
|
|
940
980
|
scheduleClose(() => {
|
|
941
981
|
if (typeof onComplete === 'function')
|
|
@@ -943,7 +983,7 @@ function TrainingPagePopupInner(props) {
|
|
|
943
983
|
if (typeof onBack === 'function')
|
|
944
984
|
onBack();
|
|
945
985
|
});
|
|
946
|
-
}, [onComplete, onBack,
|
|
986
|
+
}, [onComplete, onBack, 판끝남적기, setResultOverlay, scheduleClose]);
|
|
947
987
|
const handleNoticeClose = useCallback(() => setNoticeMessage(''), [setNoticeMessage]);
|
|
948
988
|
const handleCloseBack = useCallback(() => {
|
|
949
989
|
scheduleClose(() => { if (typeof onBack === 'function')
|
|
@@ -10,6 +10,21 @@ export interface TrainingBlockButtonPopupProps {
|
|
|
10
10
|
correct: number | null;
|
|
11
11
|
total: number | null;
|
|
12
12
|
}) => void;
|
|
13
|
+
/**
|
|
14
|
+
* **판이 끝난 그 순간** 호출 — 닫기와 무관하다(`TrainingPagePopup` 으로 그대로 전달).
|
|
15
|
+
*
|
|
16
|
+
* `onComplete` 는 **닫을 때** 불린다. 숙제처럼 *"이 문항 풀었다"* 를 세는 쪽은 닫기를
|
|
17
|
+
* 기다릴 수 없다 — 다 풀고 안 닫고 떠나면 통째로 날아갔다(말귀 요청 (100), 0.300.0).
|
|
18
|
+
*
|
|
19
|
+
* **값은 `...rest` 로 흘러 이미 갔는데 타입만 빠져 있었다**(0.300.0). 쓰는 쪽은 통과시킬
|
|
20
|
+
* 길이 없어 `as` 로 덮어야 했다 — (74) 가 앱까지 안 닿았던 것과 **같은 꼴**이다:
|
|
21
|
+
* 가운데 한 겹이 안 따라갔다.
|
|
22
|
+
*/
|
|
23
|
+
onRoundFinished?: (result: {
|
|
24
|
+
correct: number | null;
|
|
25
|
+
total: number | null;
|
|
26
|
+
attempt?: number;
|
|
27
|
+
}) => void;
|
|
13
28
|
popupTitle: string;
|
|
14
29
|
popupSubTitle?: string;
|
|
15
30
|
depth?: string;
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"more": "더보기",
|
|
41
41
|
"moreFor": "{{name}} 더보기",
|
|
42
42
|
"collapseFor": "{{name}} 접기",
|
|
43
|
-
"expandFor": "{{name}}
|
|
43
|
+
"expandFor": "{{name}} 펼치기",
|
|
44
44
|
"sortFor": "{{name}} 정렬",
|
|
45
45
|
"retry": "다시 시도",
|
|
46
46
|
"prevPage": "이전 페이지",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"pageSizeLabel": "{{n}}개씩 보기",
|
|
64
64
|
"totalCountPrefix": "총 ",
|
|
65
65
|
"totalCountSuffix": "개",
|
|
66
|
-
"expand": "
|
|
66
|
+
"expand": "펼치기",
|
|
67
67
|
"collapse": "접기",
|
|
68
68
|
"popup": "팝업",
|
|
69
69
|
"codeBlock": "코드 블록",
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
},
|
|
160
160
|
"accordion": {
|
|
161
161
|
"collapse": "접기",
|
|
162
|
-
"expand": "
|
|
162
|
+
"expand": "펼치기"
|
|
163
163
|
},
|
|
164
164
|
"header": {
|
|
165
165
|
"biArea": "BI 영역",
|