@ihabdevteam/core 0.9.6 → 0.9.8

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,7 +1,23 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import './DebugTooltip.css';
3
- /** @public */
4
- export default function DebugResultLauncher({ renderResultPopup }?: {
3
+ /**
4
+ * 결과 팝업을 실제 훈련 없이 열어보는 개발용 단추 넷.
5
+ *
6
+ * 예전에는 파일 안의 `const DEBUG = false` 로만 켤 수 있었다. 모노레포 시절에는
7
+ * packages/core 를 그 자리에서 고칠 수 있었으니 말이 됐는데, **패키지로 분리되면서
8
+ * 켤 방법이 사라졌다** — 소비자가 node_modules 를 고치지 않는 한 언제나 null 이다.
9
+ * 실제로 speechear-v2 가 이것을 렌더하고 있고, 아무것도 그려지지 않는다.
10
+ *
11
+ * 그래서 스위치를 밖으로 낸다. 기본값은 그대로 꺼짐이라 지금 쓰는 곳은 안 바뀐다.
12
+ *
13
+ * @example
14
+ * <DebugResultLauncher enabled={import.meta.env.DEV} renderResultPopup={…} />
15
+ *
16
+ * @public
17
+ */
18
+ export default function DebugResultLauncher({ enabled, renderResultPopup }?: {
19
+ /** 켜야 단추가 보인다. 기본은 꺼짐 — 배포 화면에 실수로 남지 않게. */
20
+ enabled?: boolean;
5
21
  renderResultPopup?: (props: {
6
22
  score?: number | null;
7
23
  contextsLength?: number | null;
@@ -2,9 +2,22 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useCallback } from 'react';
4
4
  import './DebugTooltip.css';
5
- const DEBUG = false;
6
- /** @public */
7
- export default function DebugResultLauncher({ renderResultPopup } = {}) {
5
+ /**
6
+ * 결과 팝업을 실제 훈련 없이 열어보는 개발용 단추 넷.
7
+ *
8
+ * 예전에는 파일 안의 `const DEBUG = false` 로만 켤 수 있었다. 모노레포 시절에는
9
+ * packages/core 를 그 자리에서 고칠 수 있었으니 말이 됐는데, **패키지로 분리되면서
10
+ * 켤 방법이 사라졌다** — 소비자가 node_modules 를 고치지 않는 한 언제나 null 이다.
11
+ * 실제로 speechear-v2 가 이것을 렌더하고 있고, 아무것도 그려지지 않는다.
12
+ *
13
+ * 그래서 스위치를 밖으로 낸다. 기본값은 그대로 꺼짐이라 지금 쓰는 곳은 안 바뀐다.
14
+ *
15
+ * @example
16
+ * <DebugResultLauncher enabled={import.meta.env.DEV} renderResultPopup={…} />
17
+ *
18
+ * @public
19
+ */
20
+ export default function DebugResultLauncher({ enabled = false, renderResultPopup } = {}) {
8
21
  const [openType, setOpenType] = useState(null);
9
22
  const [payload, setPayload] = useState(null);
10
23
  const openPopup = (type) => {
@@ -66,8 +79,8 @@ export default function DebugResultLauncher({ renderResultPopup } = {}) {
66
79
  const handleShortPopup = useCallback(() => openPopup('short'), []);
67
80
  const handleLongPopup = useCallback(() => openPopup('long'), []);
68
81
  const handleOrderPopup = useCallback(() => openPopup('order'), []);
69
- // 훅 규칙 준수를 위해 모든 훅 호출 뒤에서 분기(DEBUG는 컴파일 타임 상수)
70
- if (!DEBUG)
82
+ // 훅 규칙 준수를 위해 모든 훅 호출 뒤에서 분기한다 훅들은 언제나 같은 수만큼 돈다.
83
+ if (!enabled)
71
84
  return null;
72
85
  return (_jsxs("div", { className: "debug-result-launcher", children: [_jsxs("div", { className: "debug-buttons", children: [_jsx("button", { className: "debug-button", onClick: handleNoisePopup, children: "Noise \uACB0\uACFC\uBCF4\uAE30" }), _jsx("button", { className: "debug-button", onClick: handleShortPopup, children: "ShortStory \uACB0\uACFC\uBCF4\uAE30" }), _jsx("button", { className: "debug-button", onClick: handleLongPopup, children: "LongStory \uACB0\uACFC\uBCF4\uAE30" }), _jsx("button", { className: "debug-button", onClick: handleOrderPopup, children: "Order \uACB0\uACFC\uBCF4\uAE30" })] }), openType && payload ? renderResultPopup?.({
73
86
  score: payload.score,
@@ -61,31 +61,34 @@ export default function TrainingStepConfirm({ index, stepIndex, isLast = false,
61
61
  /* 여기 오면 늘 active 다 — canNotify 는 아래 흐름을 그대로 두기 위해 남긴다. */
62
62
  const canNotify = currentState === 'active' || currentState === 'done';
63
63
  if (multiSelect) {
64
- setSelectedSet((prev) => {
65
- const next = new Set(prev);
66
- const wasSelected = next.has(val);
67
- if (wasSelected)
68
- next.delete(val);
69
- else
70
- next.add(val);
71
- if (typeof onChange === 'function') {
72
- onChange({ selectedValues: [...next], __ts: Date.now() });
73
- }
74
- if (canNotify) {
75
- // correctCount가 지정된 문제는 그 개수를 다 채워야 제출된다 — 지정 안 됐을 때만
76
- // (예: 정답이 정해지지 않은 단일 선택형) 아무거나 하나 고르면 바로 제출한다.
77
- // 이전엔 "|| !wasSelected"로 묶여 있어 correctCount 지정 여부와 무관하게
78
- // 추가 선택마다 항상 true가 되어, 목표 개수를 채우기 전에도 매번 제출됐다.
79
- const hasCorrectCount = typeof correctCount === 'number' && correctCount > 0;
80
- const shouldSubmitNow = hasCorrectCount ? next.size >= correctCount : !wasSelected;
81
- if (shouldSubmitNow && typeof onDone === 'function') {
82
- if (currentState === 'active') {
83
- onDone({ selectedValues: [...next], __ts: Date.now() });
84
- }
64
+ /* 다음 선택 집합을 **updater 밖에서** 만든다. 예전에는 updater 안에서 만들면서
65
+ 거기서 onChange/onDone 까지 불렀는데, updater 는 순수해야 하고 React 가 렌더
66
+ 단계에 부를 있다 — 그래서 "렌더 중에 다른 컴포넌트를 갱신한다" 는 경고가
67
+ 났다(실측). 클릭은 discrete event 라 처리 시점에 상태가 이미 커밋돼 있으므로
68
+ 여기서 selectedSet 을 읽어도 어긋나지 않는다. */
69
+ const next = new Set(selectedSet);
70
+ const wasSelected = next.has(val);
71
+ if (wasSelected)
72
+ next.delete(val);
73
+ else
74
+ next.add(val);
75
+ setSelectedSet(next);
76
+ if (typeof onChange === 'function') {
77
+ onChange({ selectedValues: [...next], __ts: Date.now() });
78
+ }
79
+ if (canNotify) {
80
+ // correctCount가 지정된 문제는 개수를 채워야 제출된다 — 지정 안 됐을 때만
81
+ // (예: 정답이 정해지지 않은 단일 선택형) 아무거나 하나 고르면 바로 제출한다.
82
+ // 이전엔 "|| !wasSelected"로 묶여 있어 correctCount 지정 여부와 무관하게
83
+ // 추가 선택마다 항상 true가 되어, 목표 개수를 채우기 전에도 매번 제출됐다.
84
+ const hasCorrectCount = typeof correctCount === 'number' && correctCount > 0;
85
+ const shouldSubmitNow = hasCorrectCount ? next.size >= correctCount : !wasSelected;
86
+ if (shouldSubmitNow && typeof onDone === 'function') {
87
+ if (currentState === 'active') {
88
+ onDone({ selectedValues: [...next], __ts: Date.now() });
85
89
  }
86
90
  }
87
- return next;
88
- });
91
+ }
89
92
  }
90
93
  else {
91
94
  setSelectedAnswer(val);
@@ -98,20 +101,17 @@ export default function TrainingStepConfirm({ index, stepIndex, isLast = false,
98
101
  }
99
102
  }
100
103
  }
101
- }, [choices, currentState, multiSelect, onChange, onDone, correctCount]);
102
- useEffect(() => {
103
- if (!multiSelect || selectedSet.size === 0)
104
- return;
105
- if (typeof onChange === 'function') {
106
- onChange({ selectedValues: [...selectedSet], __ts: Date.now() });
107
- }
108
- if (typeof correctCount === 'number' && correctCount > 0) {
109
- if (selectedSet.size >= correctCount && currentState === 'active') {
110
- if (typeof onDone === 'function')
111
- onDone({ selectedValues: [...selectedSet], __ts: Date.now() });
112
- }
113
- }
114
- }, [selectedSet, multiSelect, onChange, correctCount, currentState, onDone]);
104
+ }, [choices, currentState, multiSelect, onChange, onDone, correctCount, selectedSet]);
105
+ /* 여기 있던 effect 는 selectedSet 이 바뀔 때마다 onChange/onDone 을 **한 번 더**
106
+ 불렀다. 클릭 핸들러가 이미 같은 일을 하고 있어 한 번 고를 때마다 두 번 보고됐다.
107
+ 게다가 콜백이 그 effect 의 의존성에 있어서, 부르는 쪽이 인라인 화살표를 주고
108
+ 받은 payload 를 담으면(payload Date.now() 가 있어 담는 값이 매번 다르다)
109
+ onChange 부모 리렌더 새 화살표 → effect → onChange … 로 끝나지 않았다.
110
+ 실측: 고르기 한 번에 40회(세운 상한). 상한을 빼면 10분이 지나도 안 끝났다.
111
+
112
+ 지울 수 있는 까닭은 selectedSet 실제로 채우는 곳이 클릭 핸들러 하나뿐이기
113
+ 때문이다 — 나머지 둘(choices 바뀜·페이지 전환)은 빈 Set 으로 되돌리는 자리라
114
+ effect `size === 0` 가드에 걸려 어차피 아무것도 안 했다. */
115
115
  const btnsClassName = `training-step__buttons${Array.isArray(choices) && choices.length > 3 ? ' training-step__buttons--wrap-3' : ''}`;
116
116
  return (_jsx(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, { 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 }) }));
117
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ihabdevteam/core",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
4
4
  "private": false,
5
5
  "main": "dist/ihabdevteam-core.cjs.js",
6
6
  "module": "dist/ihabdevteam-core.esm.js",