@ihabdevteam/core 0.342.0 → 0.344.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +103 -2
  2. package/dist/components.cjs.js +40 -40
  3. package/dist/hooks.cjs.js +5 -3
  4. package/dist/i18n.cjs.js +1 -1
  5. package/dist/ihabdevteam-core.cjs.js +188 -188
  6. package/dist/ihabdevteam-core.d.ts +2 -2
  7. package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +4 -3
  8. package/dist/packages/core/src/components/Training/TrainingPagePopup.js +14 -1
  9. package/dist/packages/core/src/hooks/useCacheManagement.js +15 -5
  10. package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +16 -4
  11. package/dist/packages/core/src/hooks/useEditableList.js +11 -2
  12. package/dist/packages/core/src/hooks/useFocusTrap.js +11 -1
  13. package/dist/packages/core/src/hooks/useIcon.js +7 -1
  14. package/dist/packages/core/src/hooks/useInlineSvg.js +47 -18
  15. package/dist/packages/core/src/hooks/useListControls.js +11 -7
  16. package/dist/packages/core/src/hooks/usePasswordChange.js +15 -1
  17. package/dist/packages/core/src/hooks/useSoundCompassExam.js +5 -3
  18. package/dist/packages/core/src/hooks/useTablePaging.js +10 -5
  19. package/dist/packages/core/src/hooks/useTrainingSessionLog.js +8 -4
  20. package/dist/packages/core/src/hooks/useVisitAllPanel.d.ts +5 -4
  21. package/dist/packages/core/src/hooks/useVisitAllPanel.js +11 -3
  22. package/dist/packages/core/src/hooks/useWordImage.js +15 -7
  23. package/dist/packages/core/src/hooks/useWordMeanings.js +36 -9
  24. package/dist/packages/core/src/hooks/useWordTrainingFilter.js +9 -5
  25. package/dist/packages/core/src/utils/getTrainingData.js +9 -2
  26. package/dist/packages/core/src/utils/imageConfig.js +52 -17
  27. package/dist/packages/core/src/utils/prefetchChunk.js +10 -3
  28. package/dist/packages/core/src/utils/speak.js +51 -27
  29. package/dist/packages/core/src/utils/ttsConfig.js +10 -3
  30. package/dist/packages/core/src/utils/wordPairIndex.js +16 -1
  31. package/dist/packages/core/src/version.d.ts +1 -1
  32. package/dist/packages/core/src/version.js +1 -1
  33. package/dist/utils.cjs.js +4 -4
  34. package/package.json +1 -1
@@ -34,18 +34,41 @@ export function useWordMeanings(rows, fetchMeanings) {
34
34
  /* 캐시는 모듈 스코프 Map 이라 갱신이 렌더에 자동 반영되지 않는다 — 조회가 끝나면 이 값을
35
35
  올려 아래 스냅샷을 다시 계산시킨다(캐시 히트만으로 리렌더가 연쇄되지 않도록 분리). */
36
36
  const [fetched, setFetched] = useState(0);
37
+ const [retry, setRetry] = useState(0);
37
38
  // rows 는 매 렌더 새 배열이라 의존성으로 쓸 수 없다 — 조회 키만 뽑아 문자열로 비교한다
38
39
  // 차례는 조회와 무관하다 — 정렬해 두어야 같은 낱말들을 다시 정렬만 해도 새 조회로 보지 않는다
39
- const wantedKey = Array.from(new Set(rows.map(r => meaningKey(r.word, r.homonum)))).sort().join('|');
40
+ const wantedKey = JSON.stringify(Array.from(new Set(rows.map(r => meaningKey(r.word, r.homonum)))).sort());
40
41
  useEffect(() => {
41
- const wanted = wantedKey ? wantedKey.split('|') : [];
42
+ const wanted = JSON.parse(wantedKey);
42
43
  const now = Date.now();
43
44
  const missing = wanted.filter(k => !memCache.has(k) && !noMeaning.has(k) && !(now - (emptyAt.get(k) ?? -Infinity) < EMPTY_GRACE_MS));
44
- if (missing.length === 0)
45
- return; // 전부 캐시에 있음 — 네트워크·상태 갱신 불필요
46
45
  let cancelled = false;
46
+ let retryTimer = null;
47
+ let retryDue = Infinity;
48
+ const scheduleRetry = (delay = EMPTY_GRACE_MS) => {
49
+ if (cancelled)
50
+ return;
51
+ const due = Date.now() + delay;
52
+ if (due >= retryDue)
53
+ return;
54
+ if (retryTimer !== null)
55
+ clearTimeout(retryTimer);
56
+ retryDue = due;
57
+ retryTimer = setTimeout(() => setRetry(n => n + 1), delay);
58
+ };
59
+ // 유예 중 화면을 닫았다 다시 열어도 만료 시 재조회한다.
60
+ for (const k of wanted) {
61
+ if (memCache.has(k) || noMeaning.has(k))
62
+ continue;
63
+ const at = emptyAt.get(k);
64
+ if (at != null && now - at < EMPTY_GRACE_MS)
65
+ scheduleRetry(Math.max(0, EMPTY_GRACE_MS - (now - at)));
66
+ }
67
+ if (missing.length === 0)
68
+ return () => { cancelled = true; if (retryTimer !== null)
69
+ clearTimeout(retryTimer); };
47
70
  const words = Array.from(new Set(missing.map(k => k.slice(0, k.lastIndexOf(':')))));
48
- void fetchMeanings(words).then(got => {
71
+ void Promise.resolve().then(() => fetchMeanings(words)).then(got => {
49
72
  /* 받은 것은 **취소됐어도 캐시에 담는다** — 쪽을 빨리 넘기면 앞 조회가 취소되며 받은 결과까지 버려, 돌아오면 또
50
73
  조회했다. 화면 갱신(setFetched)만 취소된 쪽에서 건너뛴다. */
51
74
  for (const row of got) {
@@ -63,18 +86,22 @@ export function useWordMeanings(rows, fetchMeanings) {
63
86
  const t = Date.now();
64
87
  for (const k of missing)
65
88
  emptyAt.set(k, t);
89
+ // 화면을 그대로 보고 있어도 유예가 끝나면 다시 묻는다.
90
+ scheduleRetry();
66
91
  }
67
92
  if (cancelled || got.length === 0)
68
93
  return;
69
94
  setFetched(v => v + 1);
70
95
  }).catch(() => {
71
96
  // 조회 함수가 약속을 어기고 거절해도(망 끊김 등) 처리되지 않은 거절로 새지 않게 삼킨다 —
72
- // 뜻은 있으면 좋은 것이라 빈 결과와 같게 다룬다. 캐시에 안 넣었으니 다음에 다시 받는다.
97
+ // 뜻은 있으면 좋은 것이라 빈 결과와 같게 다룬다. 같은 화면에서도 잠시 뒤 다시 묻는다.
98
+ scheduleRetry();
73
99
  });
74
- return () => { cancelled = true; };
75
- }, [wantedKey, fetchMeanings]);
100
+ return () => { cancelled = true; if (retryTimer !== null)
101
+ clearTimeout(retryTimer); };
102
+ }, [wantedKey, fetchMeanings, retry]);
76
103
  return useMemo(() => {
77
- const wanted = wantedKey ? wantedKey.split('|') : [];
104
+ const wanted = JSON.parse(wantedKey);
78
105
  const out = new Map();
79
106
  for (const k of wanted) {
80
107
  const v = memCache.get(k);
@@ -257,12 +257,16 @@ export function useWordTrainingFilter(groupKey, caps, opts) {
257
257
  if (cancelled)
258
258
  return;
259
259
  setViable(v);
260
- // 빈 결과 = 단어 풀 미로드 상태. 준비됨으로 보지 않아 저장된 선택을 성급히 해제하지 않는다.
261
- if (v.cho.size + v.jung.size + v.jong.size > 0)
262
- setViableReady(true);
260
+ // 빈 결과 = 단어 풀 미로드 상태. 앞서 한 번 준비됐더라도 갱신 중엔 다시
261
+ // 준비 안 됨으로 돌려 저장된 음소 선택을 빈 집합으로 검증하지 않는다.
262
+ setViableReady(v.cho.size + v.jung.size + v.jong.size > 0);
263
263
  })
264
- .catch(() => { if (!cancelled)
265
- setViable({ cho: new Set(), jung: new Set(), jong: new Set(), syllable: new Set() }); });
264
+ .catch(() => {
265
+ if (!cancelled) {
266
+ setViable({ cho: new Set(), jung: new Set(), jong: new Set(), syllable: new Set() });
267
+ setViableReady(false);
268
+ }
269
+ });
266
270
  return () => { cancelled = true; };
267
271
  }, [caps.choseong, difficulty, excludeNoImage, poolVersion]);
268
272
  // 현재 필터 조건으로 가능한 정답 단어 수 계산 (버튼 선택이 바뀔 때마다)
@@ -108,8 +108,12 @@ export async function loadTrainingData() {
108
108
  중복이 쌓였다(하루 선택이 중복 문항을 뽑고 기기마다 세트가 달라짐). */
109
109
  const received = { noise: [], short: [], long: [], order: [] };
110
110
  try {
111
- for (const key of Object.keys(map || {})) {
112
- const groups = map[key] || [];
111
+ if (typeof map !== 'object' || Array.isArray(map))
112
+ throw new Error('Invalid training data map');
113
+ for (const key of Object.keys(map)) {
114
+ const groups = map[key];
115
+ if (!Array.isArray(groups))
116
+ throw new Error('Invalid training data group');
113
117
  for (const g of groups) {
114
118
  // 한 행이 null·글자면 건너뛴다 — 그 한 행의 TypeError 로 자료 전체를 버리고 매번 같은 실패를 되풀이했다
115
119
  if (!g || typeof g !== 'object')
@@ -154,6 +158,9 @@ export async function loadTrainingData() {
154
158
  ORDER_SETS.push(...received.order);
155
159
  // 다 채운 뒤 _loaded 를 먼저 세운다 — 'updated' 를 받은 쪽이 isTrainingDataLoaded() 로 확인하면 아직 false 였다
156
160
  _loaded = true;
161
+ // 데이터가 오기 전에 계산한 빈 세트가 같은 날짜·개수의 캐시로 남을 수 있다.
162
+ // 어제 세트도 옛 풀을 재사용하지 않도록 캐시 전체를 무효화한다.
163
+ _cachedSelection = null;
157
164
  try {
158
165
  computeDailySelection();
159
166
  }
@@ -27,7 +27,14 @@ function ensureLoaded() {
27
27
  const v = raw ? JSON.parse(raw) : null;
28
28
  /* 객체가 아닌 JSON(null·"x"·42·배열)이 들어 있으면 `k in map`·Object.values 가 부를 때마다 던지고 memMap 이 null 로 남아
29
29
  매번 다시 파싱했다 — 확장 프로그램·손으로 고친 값이라도 캐시 비우기 전까지 이미지 확인·훈련 생성이 막혔다. */
30
- memMap = v && typeof v === 'object' && !Array.isArray(v) ? v : {};
30
+ const valid = {};
31
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
32
+ for (const [key, value] of Object.entries(v)) {
33
+ if (typeof value === 'boolean')
34
+ valid[key] = value;
35
+ }
36
+ }
37
+ memMap = valid;
31
38
  }
32
39
  catch {
33
40
  memMap = {};
@@ -36,7 +43,7 @@ function ensureLoaded() {
36
43
  }
37
44
  let savePending = null;
38
45
  function scheduleSave() {
39
- if (savePending)
46
+ if (savePending !== null)
40
47
  return;
41
48
  savePending = setTimeout(() => {
42
49
  savePending = null;
@@ -51,31 +58,42 @@ function scheduleSave() {
51
58
  function keyOf(word, homonum) {
52
59
  return `${word}_${homonum ?? 1}`;
53
60
  }
54
- /**
55
- * @public
56
- * 단어 이미지 존재 여부 확인 — localStorage 캐시 우선, 없으면 HEAD 요청 후 캐시 저장.
57
- * 한 번 확인된 단어는 영구 캐싱되어 다음 접속 시에도 즉시 반환.
58
- */
59
- export async function checkImageExists(word, homonum) {
61
+ async function probeImageExists(word, homonum, force = false) {
60
62
  const map = ensureLoaded();
61
63
  const k = keyOf(word, homonum);
62
- if (k in map)
64
+ if (!force && k in map)
63
65
  return map[k];
64
- if (inflight.has(k))
66
+ if (!force && inflight.has(k))
65
67
  return inflight.get(k);
66
68
  const resolver = getImageResolver();
67
69
  if (!resolver)
68
70
  return false;
69
- const url = resolver(word, homonum);
71
+ let url;
72
+ try {
73
+ url = resolver(word, homonum);
74
+ }
75
+ catch {
76
+ return false;
77
+ }
70
78
  if (!url)
71
79
  return false;
72
80
  // 타임아웃 없으면 HEAD 요청 하나가 응답 없이 걸릴 때 이 확인을 기다리는
73
81
  // 검색 버튼(이미지 있음 필터)이 영구 비활성화된다
74
- /* **확인이 안 된 것은 캐시하지 않는다.** 망 끊김·타임아웃·5xx 까지 '이미지 없음' 으로 영구 저장해, 한 번 끊긴 단어는 망이
75
- 돌아와도 '이미지 있음' 필터에서 계속 빠졌다(ttsConfig.checkAudioExists 와 같게). 있음(2xx)·없음(4xx)만 담는다. */
76
- const p = fetch(url, { method: 'HEAD', signal: timeoutSignal(8000) })
77
- .then(res => (res.status >= 500 ? null : res.ok), () => null)
82
+ /* **확인이 안 된 것은 캐시하지 않는다.** 망 끊김·타임아웃·권한 오류·5xx 는 이미지가 없다는 증거가 아니다.
83
+ 있음(2xx)과 명시적 없음(404·410)만 담는다. */
84
+ let request;
85
+ try {
86
+ request = fetch(url, { method: 'HEAD', signal: timeoutSignal(8000) });
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ const p = request
92
+ .then(res => (res.ok ? true : res.status === 404 || res.status === 410 ? false : null), () => null)
78
93
  .then(ok => {
94
+ // 강제 재확인이 앞 요청을 대체했거나 캐시를 비웠다면 늦은 응답으로 새 값을 덮지 않는다.
95
+ if (inflight.get(k) !== p)
96
+ return ok ?? false;
79
97
  inflight.delete(k);
80
98
  if (ok === null)
81
99
  return false;
@@ -87,6 +105,14 @@ export async function checkImageExists(word, homonum) {
87
105
  inflight.set(k, p);
88
106
  return p;
89
107
  }
108
+ /**
109
+ * @public
110
+ * 단어 이미지 존재 여부 확인 — localStorage 캐시 우선, 없으면 HEAD 요청 후 캐시 저장.
111
+ * 한 번 확인된 단어는 영구 캐싱되어 다음 접속 시에도 즉시 반환.
112
+ */
113
+ export async function checkImageExists(word, homonum) {
114
+ return probeImageExists(word, homonum);
115
+ }
90
116
  /** @public 단어 이미지 존재 여부 동기 조회 (캐시에 있을 때만). 없으면 undefined. */
91
117
  export function getImageExistsCached(word, homonum) {
92
118
  const map = ensureLoaded();
@@ -107,7 +133,8 @@ export async function prewarmImageCache(words, options = {}) {
107
133
  let done = 0;
108
134
  if (todo.length > 0) {
109
135
  let idx = 0;
110
- const workers = Array.from({ length: Math.min(concurrency, todo.length) }, async () => {
136
+ const workerCount = Math.min(todo.length, Math.max(1, Number.isFinite(concurrency) ? Math.floor(concurrency) : 16));
137
+ const workers = Array.from({ length: workerCount }, async () => {
111
138
  while (idx < todo.length) {
112
139
  const i = idx++;
113
140
  const w = todo[i];
@@ -118,7 +145,7 @@ export async function prewarmImageCache(words, options = {}) {
118
145
  continue;
119
146
  }
120
147
  try {
121
- await checkImageExists(w);
148
+ await probeImageExists(w, undefined, force);
122
149
  }
123
150
  catch { }
124
151
  done++;
@@ -170,6 +197,9 @@ export function getImageOkWordSet() {
170
197
  */
171
198
  export function bulkSetImageExistence(entries) {
172
199
  const map = ensureLoaded();
200
+ // 일괄 주입이 정본이다. 앞서 시작한 HEAD가 늦게 끝나 새 값을 덮지 못하게 한다.
201
+ for (const k of Object.keys(entries))
202
+ inflight.delete(k);
173
203
  Object.assign(map, entries);
174
204
  cacheVersion++;
175
205
  try {
@@ -181,7 +211,12 @@ export function bulkSetImageExistence(entries) {
181
211
  }
182
212
  /** @public 캐시 초기화 (이미지 데이터 변경 시 호출) */
183
213
  export function clearImageExistenceCache() {
214
+ if (savePending !== null) {
215
+ clearTimeout(savePending);
216
+ savePending = null;
217
+ }
184
218
  memMap = {};
219
+ inflight.clear();
185
220
  cacheVersion++;
186
221
  try {
187
222
  if (typeof localStorage !== 'undefined')
@@ -27,9 +27,16 @@
27
27
  */
28
28
  export function prefetchChunk(...loads) {
29
29
  let cancelled = false;
30
- const load = () => { if (!cancelled)
31
- for (const load of loads)
32
- void load().catch(() => { }); };
30
+ const load = () => {
31
+ if (cancelled)
32
+ return;
33
+ for (const load of loads) {
34
+ try {
35
+ void load().catch(() => { });
36
+ }
37
+ catch { /* 미리 받기 실패는 화면 전환 때 다시 시도한다 */ }
38
+ }
39
+ };
33
40
  const idle = globalThis.requestIdleCallback;
34
41
  if (idle) {
35
42
  const id = idle(load, { timeout: 2000 });
@@ -15,40 +15,64 @@ export function speak(text, { onEnd, rate = 0.9 } = {}) {
15
15
  }
16
16
  const synth = window.speechSynthesis;
17
17
  clearPendingWait();
18
+ let finished = false;
19
+ const finish = () => {
20
+ if (finished)
21
+ return;
22
+ finished = true;
23
+ try {
24
+ onEnd?.();
25
+ }
26
+ catch (e) { }
27
+ };
28
+ // 새 말을 위한 목소리 목록을 기다리는 동안에도 이전 발화는 바로 멈춘다.
29
+ try {
30
+ synth.cancel();
31
+ }
32
+ catch (e) {
33
+ finish();
34
+ return;
35
+ }
18
36
  const doSpeak = () => {
19
37
  clearPendingWait();
20
- synth.cancel();
21
- const u = new SpeechSynthesisUtterance(text);
22
- const voices = synth.getVoices();
23
- const ko = voices.find(v => v.lang.startsWith('ko'));
24
- if (ko)
25
- u.voice = ko;
26
- u.rate = rate;
27
- u.onend = onEnd
28
- ? function (_ev) {
29
- try {
30
- onEnd();
31
- }
32
- catch (e) { }
33
- }
34
- : null;
35
- // 합성이 실패하면 end 없이 error 만 온다 — 끝난 것으로 알린다(안 그러면 onEnd 를 기다리는 쪽이 멈춘 채)
36
- u.onerror = onEnd
37
- ? function () {
38
- try {
39
- onEnd();
40
- }
41
- catch (e) { }
42
- }
43
- : null;
44
- synth.speak(u);
38
+ try {
39
+ synth.cancel();
40
+ const u = new SpeechSynthesisUtterance(text);
41
+ const voices = synth.getVoices();
42
+ const ko = voices.find(v => v.lang.startsWith('ko'));
43
+ if (ko)
44
+ u.voice = ko;
45
+ u.rate = rate;
46
+ u.onend = onEnd ? finish : null;
47
+ // 합성이 실패하면 end 없이 error 만 온다 — 끝난 것으로 알린다(안 그러면 onEnd 를 기다리는 쪽이 멈춘 채)
48
+ u.onerror = onEnd ? finish : null;
49
+ synth.speak(u);
50
+ }
51
+ catch (e) {
52
+ finish();
53
+ }
45
54
  };
46
- if (synth.getVoices().length > 0) {
55
+ let voices;
56
+ try {
57
+ voices = synth.getVoices();
58
+ }
59
+ catch (e) {
60
+ finish();
61
+ return;
62
+ }
63
+ if (voices.length > 0) {
47
64
  doSpeak();
48
65
  return;
49
66
  }
50
67
  const timer = setTimeout(doSpeak, VOICE_WAIT_MS);
51
- synth.addEventListener('voiceschanged', doSpeak, { once: true });
68
+ try {
69
+ synth.addEventListener('voiceschanged', doSpeak, { once: true });
70
+ }
71
+ catch (e) {
72
+ clearTimeout(timer);
73
+ finish();
74
+ return;
75
+ }
52
76
  pendingWait = {
53
77
  clear: () => {
54
78
  clearTimeout(timer);
@@ -76,7 +76,7 @@ export async function checkAudioExists(word) {
76
76
  return audioExistsCache.get(url);
77
77
  // 타임아웃 없으면 HEAD 요청 하나가 응답 없이 걸릴 때 이 확인을 기다리는
78
78
  // 검색 버튼(음성 있음 필터)이 영구 비활성화된다
79
- // 서버가 답을 준 경우(2xx/4xx)만 캐시에 남긴다. 네트워크 실패·타임아웃·5xx 까지 캐시하면
79
+ // 성공(2xx)과 명시적 부재(404·410)만 캐시에 남긴다. 권한 오류·네트워크 실패·타임아웃·5xx 까지 캐시하면
80
80
  // 한 번의 통신 끊김·서버 장애가 그 단어를 세션 내내 '음성 없음'으로 고정해, '음성 있음' 필터에서
81
81
  // 통째로 빠진다. 실패는 지워서 다음 호출이 다시 물어보게 한다 — 단 **이 요청이 담은 칸일 때만**:
82
82
  // clearAudioExistsCache() 뒤 새로 담긴 확인을 늦게 끝난 옛 요청이 지우면 같은 주소를 또 묻는다.
@@ -85,8 +85,15 @@ export async function checkAudioExists(word) {
85
85
  audioExistsCache.delete(url);
86
86
  return false;
87
87
  };
88
- const promise = fetch(url, { method: 'HEAD', signal: timeoutSignal(8000) })
89
- .then(res => (res.status >= 500 ? onUnconfirmed() : res.ok), onUnconfirmed);
88
+ let request;
89
+ try {
90
+ request = fetch(url, { method: 'HEAD', signal: timeoutSignal(8000) });
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ const promise = request
96
+ .then(res => (res.ok ? true : res.status === 404 || res.status === 410 ? false : onUnconfirmed()), onUnconfirmed);
90
97
  audioExistsCache.set(url, promise);
91
98
  return promise;
92
99
  }
@@ -19,6 +19,20 @@ const LS_PAIR_INDEX = 'word_pair_index_v2';
19
19
  const LS_PAIR_INDEX_SIG = 'word_pair_index_sig_v2';
20
20
  let memIndex = null;
21
21
  let memSig = null;
22
+ function isPairIndex(value) {
23
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
24
+ return false;
25
+ for (const entry of Object.values(value)) {
26
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry))
27
+ return false;
28
+ const parts = entry;
29
+ for (const kind of ['cho', 'jung', 'jong']) {
30
+ if (!Array.isArray(parts[kind]) || !parts[kind].every((word) => typeof word === 'string'))
31
+ return false;
32
+ }
33
+ }
34
+ return true;
35
+ }
22
36
  function loadFromLocalStorage() {
23
37
  try {
24
38
  if (typeof localStorage === 'undefined')
@@ -27,7 +41,8 @@ function loadFromLocalStorage() {
27
41
  const sig = localStorage.getItem(LS_PAIR_INDEX_SIG);
28
42
  if (!raw || !sig)
29
43
  return null;
30
- return { index: JSON.parse(raw), sig };
44
+ const index = JSON.parse(raw);
45
+ return isPairIndex(index) ? { index, sig } : null;
31
46
  }
32
47
  catch {
33
48
  return null;
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.342.0";
49
+ export declare const CORE_VERSION = "0.344.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.342.0';
49
+ export const CORE_VERSION = '0.344.0';