@ihabdevteam/core 0.322.0 → 0.324.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 (198) hide show
  1. package/CHANGELOG.md +557 -0
  2. package/dist/components.cjs.js +183 -183
  3. package/dist/components.esm.css +1 -1
  4. package/dist/hooks.cjs.js +3 -3
  5. package/dist/i18n.cjs.js +3 -3
  6. package/dist/ihabdevteam-core.cjs.js +187 -187
  7. package/dist/ihabdevteam-core.d.ts +55 -17
  8. package/dist/packages/core/src/components/Accordion.js +3 -3
  9. package/dist/packages/core/src/components/Alert.js +2 -2
  10. package/dist/packages/core/src/components/BarChart.js +120 -119
  11. package/dist/packages/core/src/components/BottomNav.js +18 -18
  12. package/dist/packages/core/src/components/BulkBar.js +25 -25
  13. package/dist/packages/core/src/components/Button.js +16 -16
  14. package/dist/packages/core/src/components/Calendar.js +17 -17
  15. package/dist/packages/core/src/components/ChartSection.js +30 -30
  16. package/dist/packages/core/src/components/CountTile.js +38 -38
  17. package/dist/packages/core/src/components/DatePicker.js +59 -59
  18. package/dist/packages/core/src/components/Dialog.js +14 -14
  19. package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.js +8 -8
  20. package/dist/packages/core/src/components/FilterPanel.js +6 -6
  21. package/dist/packages/core/src/components/FilterSectionInfo.js +6 -6
  22. package/dist/packages/core/src/components/GameIntro.js +1 -1
  23. package/dist/packages/core/src/components/Icon.js +2 -2
  24. package/dist/packages/core/src/components/ItemActions.js +16 -16
  25. package/dist/packages/core/src/components/LandingHeader.js +6 -6
  26. package/dist/packages/core/src/components/LineChart.js +24 -24
  27. package/dist/packages/core/src/components/Loader.js +5 -5
  28. package/dist/packages/core/src/components/NoticeBox.js +3 -3
  29. package/dist/packages/core/src/components/PageTitle.js +2 -2
  30. package/dist/packages/core/src/components/Pagination.js +4 -4
  31. package/dist/packages/core/src/components/Panel.js +2 -2
  32. package/dist/packages/core/src/components/PanelRow.js +6 -13
  33. package/dist/packages/core/src/components/PassCard/PassCard.js +9 -9
  34. package/dist/packages/core/src/components/PassCard/passCardProduct.js +2 -2
  35. package/dist/packages/core/src/components/PaymentPlan.js +5 -5
  36. package/dist/packages/core/src/components/Popup/ConfirmPopup.js +29 -29
  37. package/dist/packages/core/src/components/Popup/CouponInputPopup.js +2 -2
  38. package/dist/packages/core/src/components/Popup/DeviceSideTable.js +18 -25
  39. package/dist/packages/core/src/components/Popup/ListPopup.js +3 -3
  40. package/dist/packages/core/src/components/Popup/LoadTablePopup.js +7 -7
  41. package/dist/packages/core/src/components/Popup/Popup.js +27 -27
  42. package/dist/packages/core/src/components/Popup/SaveTablePopup.js +5 -5
  43. package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +4 -4
  44. package/dist/packages/core/src/components/Popup/customerDetailUtils.js +18 -18
  45. package/dist/packages/core/src/components/Popup/useDiscardGuard.js +4 -4
  46. package/dist/packages/core/src/components/ProgressIndicator.js +5 -5
  47. package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +16 -16
  48. package/dist/packages/core/src/components/Rating.js +20 -20
  49. package/dist/packages/core/src/components/Report/ReportDocument.js +21 -21
  50. package/dist/packages/core/src/components/RingChart.js +5 -5
  51. package/dist/packages/core/src/components/SectionHeadFloat.js +15 -15
  52. package/dist/packages/core/src/components/SegmentedControl.js +34 -34
  53. package/dist/packages/core/src/components/SideNav.js +37 -37
  54. package/dist/packages/core/src/components/Slider.js +2 -2
  55. package/dist/packages/core/src/components/SoundCompass.js +5 -5
  56. package/dist/packages/core/src/components/Table.js +257 -98
  57. package/dist/packages/core/src/components/Tabs.js +12 -12
  58. package/dist/packages/core/src/components/TextField.js +34 -34
  59. package/dist/packages/core/src/components/Toast.js +6 -6
  60. package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +4 -4
  61. package/dist/packages/core/src/components/Tooltip/Tooltip.js +22 -22
  62. package/dist/packages/core/src/components/Tooltip/TrainingTooltip.js +4 -4
  63. package/dist/packages/core/src/components/TopBanner.js +28 -28
  64. package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +11 -11
  65. package/dist/packages/core/src/components/Training/CategoryIcon.js +2 -2
  66. package/dist/packages/core/src/components/Training/NoisePlayerContext.js +12 -12
  67. package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +2 -2
  68. package/dist/packages/core/src/components/Training/TrainingBlock.js +11 -11
  69. package/dist/packages/core/src/components/Training/TrainingBlockButton.js +10 -10
  70. package/dist/packages/core/src/components/Training/TrainingBlockGrid.js +21 -21
  71. package/dist/packages/core/src/components/Training/TrainingCard.css +13 -1
  72. package/dist/packages/core/src/components/Training/TrainingCard.js +2 -2
  73. package/dist/packages/core/src/components/Training/TrainingCardGroup.js +6 -6
  74. package/dist/packages/core/src/components/Training/TrainingPagePopup.js +81 -81
  75. package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +17 -17
  76. package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +127 -127
  77. package/dist/packages/core/src/components/Training/TrainingStepCard.js +18 -18
  78. package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +4 -4
  79. package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +12 -12
  80. package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +12 -12
  81. package/dist/packages/core/src/components/Training/TrainingStepTitle.js +4 -4
  82. package/dist/packages/core/src/components/Training/TrainingWordCard.js +17 -17
  83. package/dist/packages/core/src/components/Training/WordTablePopup.js +5 -5
  84. package/dist/packages/core/src/components/Training/audioPlaybackRules.js +8 -8
  85. package/dist/packages/core/src/components/Training/audioTimeline.js +25 -25
  86. package/dist/packages/core/src/components/Training/oneShotSound.js +13 -13
  87. package/dist/packages/core/src/components/Training/trainingMissStats.js +20 -20
  88. package/dist/packages/core/src/components/Training/trainingPopupUtils.js +7 -7
  89. package/dist/packages/core/src/components/Training/useAudioPreview.js +15 -15
  90. package/dist/packages/core/src/components/Training/useFloatingFocus.js +14 -14
  91. package/dist/packages/core/src/components/Training/useNoiseFade.js +22 -22
  92. package/dist/packages/core/src/components/Training/useObjectUrls.js +2 -2
  93. package/dist/packages/core/src/components/Training/usePlaybackSequencer.d.ts +2 -2
  94. package/dist/packages/core/src/components/Training/usePlaybackSequencer.js +23 -23
  95. package/dist/packages/core/src/components/Training/useProgressTicker.js +33 -33
  96. package/dist/packages/core/src/components/Training/useTTSPlayback.js +17 -17
  97. package/dist/packages/core/src/components/Training/useWaveReady.js +11 -11
  98. package/dist/packages/core/src/components/UpdateAlert.js +6 -6
  99. package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +7 -1
  100. package/dist/packages/core/src/components/Word/TableFilterPanel.js +3 -3
  101. package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +10 -10
  102. package/dist/packages/core/src/components/Word/WordTableBlock.js +28 -28
  103. package/dist/packages/core/src/components/Word/WordTableButton.css +16 -2
  104. package/dist/packages/core/src/components/Word/WordTableThumbnail.css +6 -1
  105. package/dist/packages/core/src/components/Word/generatedDraft.js +14 -14
  106. package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -7
  107. package/dist/packages/core/src/components/Word/wordScoreStats.js +15 -15
  108. package/dist/packages/core/src/components/Word/wordTablePrint.js +10 -10
  109. package/dist/packages/core/src/components/Word/wordTableRecommend.js +11 -11
  110. package/dist/packages/core/src/components/Word/wordTableReview.js +28 -28
  111. package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +8 -11
  112. package/dist/packages/core/src/components/Word/wordTableSettings.js +48 -36
  113. package/dist/packages/core/src/components/WordTable.js +130 -130
  114. package/dist/packages/core/src/components/barChartData.d.ts +16 -1
  115. package/dist/packages/core/src/components/barChartData.js +49 -29
  116. package/dist/packages/core/src/components/barChartTypes.js +8 -8
  117. package/dist/packages/core/src/components/tableTrackList.js +11 -11
  118. package/dist/packages/core/src/components/updateAlertStack.js +28 -28
  119. package/dist/packages/core/src/components/useBarChartControl.js +4 -6
  120. package/dist/packages/core/src/components/useComboboxSuggestions.js +17 -17
  121. package/dist/packages/core/src/components/useToastLifecycle.js +18 -18
  122. package/dist/packages/core/src/components/wordTableLayout.d.ts +17 -4
  123. package/dist/packages/core/src/components/wordTableLayout.js +5 -5
  124. package/dist/packages/core/src/context/I18nProvider.js +6 -6
  125. package/dist/packages/core/src/hooks/focusReturn.js +27 -27
  126. package/dist/packages/core/src/hooks/inputModality.js +13 -13
  127. package/dist/packages/core/src/hooks/touchToggle.js +39 -39
  128. package/dist/packages/core/src/hooks/useAnchoredMenu.js +30 -30
  129. package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +28 -28
  130. package/dist/packages/core/src/hooks/useDocumentTitle.js +10 -10
  131. package/dist/packages/core/src/hooks/useEditableList.js +4 -4
  132. package/dist/packages/core/src/hooks/useFocusTrap.js +2 -2
  133. package/dist/packages/core/src/hooks/useFontScale.js +6 -6
  134. package/dist/packages/core/src/hooks/useIcon.js +7 -7
  135. package/dist/packages/core/src/hooks/useListControls.js +2 -2
  136. package/dist/packages/core/src/hooks/usePageResetOnChange.js +3 -3
  137. package/dist/packages/core/src/hooks/usePasswordChange.js +4 -4
  138. package/dist/packages/core/src/hooks/usePendingPress.js +7 -7
  139. package/dist/packages/core/src/hooks/useRelativeDay.js +4 -4
  140. package/dist/packages/core/src/hooks/useSoundCompassExam.js +6 -6
  141. package/dist/packages/core/src/hooks/useTablePaging.d.ts +0 -11
  142. package/dist/packages/core/src/hooks/useTablePaging.js +29 -34
  143. package/dist/packages/core/src/hooks/useTrainingSessionLog.js +35 -35
  144. package/dist/packages/core/src/hooks/useVisitSchedule.js +4 -4
  145. package/dist/packages/core/src/hooks/useWordAttributeFilter.js +14 -14
  146. package/dist/packages/core/src/hooks/useWordMeanings.js +5 -5
  147. package/dist/packages/core/src/hooks/useWordTrainingFilter.js +7 -7
  148. package/dist/packages/core/src/i18n/locales/en.json +14 -2
  149. package/dist/packages/core/src/i18n/locales/ko.json +13 -1
  150. package/dist/packages/core/src/types/wordSearch.d.ts +9 -0
  151. package/dist/packages/core/src/utils/audioMeter.js +5 -5
  152. package/dist/packages/core/src/utils/audioPeaks.d.ts +1 -1
  153. package/dist/packages/core/src/utils/audioPeaks.js +18 -18
  154. package/dist/packages/core/src/utils/blockGrid.js +4 -4
  155. package/dist/packages/core/src/utils/chunkSkew.js +9 -9
  156. package/dist/packages/core/src/utils/clipboard.js +3 -3
  157. package/dist/packages/core/src/utils/dailySelectionUtils.js +12 -12
  158. package/dist/packages/core/src/utils/date.js +12 -12
  159. package/dist/packages/core/src/utils/devWarn.js +4 -4
  160. package/dist/packages/core/src/utils/deviceKind.d.ts +37 -7
  161. package/dist/packages/core/src/utils/deviceKind.js +53 -10
  162. package/dist/packages/core/src/utils/errorMessage.js +7 -7
  163. package/dist/packages/core/src/utils/filterPanelSummary.js +14 -14
  164. package/dist/packages/core/src/utils/format.js +4 -4
  165. package/dist/packages/core/src/utils/getTrainingData.js +9 -9
  166. package/dist/packages/core/src/utils/imageConfig.js +17 -17
  167. package/dist/packages/core/src/utils/koreanSyllable.js +18 -18
  168. package/dist/packages/core/src/utils/navScroll.js +2 -2
  169. package/dist/packages/core/src/utils/phonemeConvert.d.ts +7 -0
  170. package/dist/packages/core/src/utils/phonemeConvert.js +19 -10
  171. package/dist/packages/core/src/utils/phonemeGroups.js +2 -2
  172. package/dist/packages/core/src/utils/phonemePairSearch.d.ts +1 -0
  173. package/dist/packages/core/src/utils/phonemePairSearch.js +129 -105
  174. package/dist/packages/core/src/utils/phonemePairSearchTypes.d.ts +2 -0
  175. package/dist/packages/core/src/utils/phonemeSectionCodes.d.ts +52 -0
  176. package/dist/packages/core/src/utils/phonemeSectionCodes.js +76 -0
  177. package/dist/packages/core/src/utils/prefetchChunk.js +6 -6
  178. package/dist/packages/core/src/utils/safeHref.js +4 -4
  179. package/dist/packages/core/src/utils/savedWordTables.js +5 -5
  180. package/dist/packages/core/src/utils/speak.js +12 -12
  181. package/dist/packages/core/src/utils/textSort.d.ts +0 -10
  182. package/dist/packages/core/src/utils/textSort.js +38 -48
  183. package/dist/packages/core/src/utils/theme.js +3 -3
  184. package/dist/packages/core/src/utils/trainingItemResults.js +3 -3
  185. package/dist/packages/core/src/utils/ttsConfig.js +2 -2
  186. package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +1 -1
  187. package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +65 -45
  188. package/dist/packages/core/src/utils/wordContrast.js +3 -3
  189. package/dist/packages/core/src/utils/wordPairIndex.js +4 -4
  190. package/dist/packages/core/src/utils/wordTrainingGenerators.js +4 -4
  191. package/dist/packages/core/src/utils/wordTrainingPool.js +15 -15
  192. package/dist/packages/core/src/utils/wordTrainingRuntime.js +6 -6
  193. package/dist/packages/core/src/utils/wornDate.d.ts +3 -3
  194. package/dist/packages/core/src/utils/wornDate.js +14 -14
  195. package/dist/packages/core/src/version.d.ts +1 -1
  196. package/dist/packages/core/src/version.js +1 -1
  197. package/dist/utils.cjs.js +4 -4
  198. package/package.json +1 -1
@@ -2,7 +2,11 @@
2
2
  .word-table-button {
3
3
  position: relative;
4
4
  box-sizing: border-box;
5
- width: 240px;
5
+ /* 280 — 대립쌍표는 한 칸에 낱말이 둘('가게 / 마개') 들어가 240 에서는 글자가 칸에 꽉 찼다.
6
+ 320 도 보았으나 한 줄에 드는 카드 수가 줄어 280 으로 정했다(사장님 지시 2026-10-13).
7
+ **이 카드를 쓰는 세 화면이 함께 바뀐다** — 같은 카드가 화면마다 다른 폭이면 그 까닭을
8
+ 나중에 아무도 설명하지 못한다. 자리의 사정이 아니라 카드 자체의 값이다. */
9
+ width: 280px;
6
10
  display: flex;
7
11
  flex-direction: column;
8
12
  border: 1px solid var(--color-whiteLine);
@@ -98,7 +102,17 @@
98
102
  }
99
103
  /* 설명 — 제목 아래, 2줄까지 표시 후 말줄임 (폰트는 body-3 토큰 그대로) */
100
104
  .word-table-button__desc {
101
- color: var(--color-gray);
105
+ /* **곁들이는 줄이라 가장 흐리다**(사장님 지시 2026-10-13, 말귀_2 청).
106
+ 제목(#111)과 `--color-gray`(#64748b)가 충분히 안 갈려 **제목과 설명이 한 덩어리로
107
+ 읽혔다.** 한 급 내려 제목이 먼저 잡히게 한다.
108
+
109
+ > **흰 바탕 2.53:1 로 본문 기준(4.5)에 못 미치는 것을 알고 그렇게 둔다.** 무엇인지는
110
+ > 제목과 썸네일이 이미 말해 주고, 이 줄은 곁들이는 설명이다. 사장님이 손잡이
111
+ > (`descTone`)와 기본값을 나란히 놓고 그 수를 보신 뒤 **기본값**으로 정하셨다.
112
+
113
+ 이 자리를 옮기거나 본문으로 승격할 일이 생기면 **대비를 먼저 다시 본다** —
114
+ `--color-gray`(4.8:1)가 그때의 기본이다. 코어에는 대비를 세는 못이 없어 사람이 본다. */
115
+ color: var(--color-lightGray);
102
116
  display: -webkit-box;
103
117
  -webkit-line-clamp: 2;
104
118
  -webkit-box-orient: vertical;
@@ -31,7 +31,12 @@
31
31
  }
32
32
 
33
33
  .word-table-thumb__word {
34
- color: var(--color-black);
34
+ /* **제목보다 한 급 흐리다**(사장님 지시 2026-10-13, 말귀_2 청).
35
+ 전에는 제목과 **둘 다 `--color-black`(#111)** 이어서 같은 무게로 서고, 눈이 어디부터
36
+ 가야 할지 안 정해졌다. 썸네일은 **무슨 표인지 알아보는 그림**이지 읽는 글이 아니다.
37
+ 카드 안의 읽는 차례를 세 겹으로 둔다 — 제목 #111 · 썸네일 #434d56 · 설명 #96a4b8.
38
+ 흰 바탕 대비 약 9.1:1 로 넉넉하다. */
39
+ color: var(--color-darkGray);
35
40
  }
36
41
 
37
42
  /* 줄간격을 좁힌다. **선택자를 한 단계 겹쳐 쓴 이유가 있다** — 쓰는 쪽이 `body-3` 을
@@ -10,14 +10,14 @@
10
10
  /** 값들을 초안으로 만든다(처음엔 아무것도 고정돼 있지 않다). */
11
11
  export function toDraft(values, keyOf) {
12
12
  const out = [];
13
- const 본것 = new Set();
13
+ const seen = new Set();
14
14
  for (const v of values) {
15
15
  const key = keyOf(v);
16
16
  /* 같은 열쇠가 두 번 오면 한 번만 담는다 — 초안에 같은 줄이 둘이면 고정·빼기가
17
17
  어느 것에 걸린 것인지 사람이 알 수 없다. */
18
- if (본것.has(key))
18
+ if (seen.has(key))
19
19
  continue;
20
- 본것.add(key);
20
+ seen.add(key);
21
21
  out.push({ key, value: v, pinned: false });
22
22
  }
23
23
  return out;
@@ -44,24 +44,24 @@ export function removeItem(draft, key) {
44
44
  * 돌연변이로 확인했다: 그 줄을 없애도 결과가 같다.
45
45
  */
46
46
  export function regenerateUnpinned(draft, candidates, keyOf) {
47
- const 고정 = draft.filter(d => d.pinned);
48
- const 고정열쇠 = new Set(고정.map(d => d.key));
49
- const 자리 = draft.length - 고정.length;
50
- const 새것 = [];
51
- const 본것 = new Set(고정열쇠);
47
+ const pinned = draft.filter(d => d.pinned);
48
+ const pinnedKeys = new Set(pinned.map(d => d.key));
49
+ const slots = draft.length - pinned.length;
50
+ const fresh = [];
51
+ const seen = new Set(pinnedKeys);
52
52
  for (const v of candidates) {
53
- if (새것.length >= 자리)
53
+ if (fresh.length >= slots)
54
54
  break; // 정확성이 아니라 효율 — 위 주석 참고
55
55
  const key = keyOf(v);
56
- if (본것.has(key))
56
+ if (seen.has(key))
57
57
  continue;
58
- 본것.add(key);
59
- 새것.push({ key, value: v, pinned: false });
58
+ seen.add(key);
59
+ fresh.push({ key, value: v, pinned: false });
60
60
  }
61
61
  /* **고정한 것의 자리를 지킨다.** 앞으로 몰아 두면, 다시 뽑을 때마다 사람이 보던 줄이
62
62
  위아래로 움직여 어디를 보고 있었는지 놓친다. */
63
- const 새것들 = [...새것];
64
- return draft.map(d => (d.pinned ? d : 새것들.shift())).filter((d) => d !== undefined);
63
+ const freshList = [...fresh];
64
+ return draft.map(d => (d.pinned ? d : freshList.shift())).filter((d) => d !== undefined);
65
65
  }
66
66
  /** 저장할 값들 — 화면에 보이는 차례 그대로. */
67
67
  export function draftValues(draft) {
@@ -3,7 +3,7 @@ import { useMemo, useState, useCallback, useEffect } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { chunkItems } from '../wordTableLayout';
5
5
  import { getImageResolver, checkImageExists } from '../../utils/imageConfig';
6
- import { buildCompositionSection, buildOrientationSection, buildBorderSection, buildCutLineSection, buildDivLineSection, buildTopInfoSection, buildBottomInfoSection, buildItemsPerPageSection, buildTableSettingsSelected, applyTableSettingsSelect, PAGE_NUM_POSITION_VALUE, parsePageNumberPosition, itemsPerPageFromChoice, } from './wordTableSettings';
6
+ import { buildCompositionSection, buildOrientationSection, buildBorderSection, buildCutLineSection, buildDivLineSection, buildTopInfoSection, buildBottomInfoSection, buildItemsPerPageSection, buildTableSettingsSelected, applyTableSettingsSelect, parsePageNumberPosition, itemsPerPageFromChoice, } from './wordTableSettings';
7
7
  /**
8
8
  * 표시 전용 단어표(편집 없음)의 '표 설정' FilterPanel 상태·섹션을 제공하는 훅.
9
9
  * 구성·용지 방향·외곽선, 내용이 있으면 페이지당 개수, 2페이지 이상이면 페이지 번호, 항목이 2개 이상이면
@@ -16,10 +16,10 @@ export function useWordTableDisplaySettings(items,
16
16
  * 안 쓰는 화면에 켜고 끌 것이 없는 절이 서면 안 되므로 기본은 거짓이다. */
17
17
  opts) {
18
18
  const { t } = useTranslation();
19
- const 띠쓰나 = opts?.paperBands === true;
19
+ const showBands = opts?.paperBands === true;
20
20
  const [orientation, setOrientation] = useState('landscape');
21
- const [contentTypes, setContentTypes] = useState(['이미지', '단어', '표준 발음']);
22
- const [cutLine, setCutLine] = useState('표시');
21
+ const [contentTypes, setContentTypes] = useState(['image', 'word', 'standardPron']);
22
+ const [cutLine, setCutLine] = useState('show');
23
23
  const [showOutline, setShowOutline] = useState(true);
24
24
  /* 종이 머리·바닥 띠 — 기본은 켬(브랜드가 찍힌 종이가 기본값이다) */
25
25
  const [showTopInfo, setShowTopInfo] = useState(true);
@@ -72,12 +72,12 @@ opts) {
72
72
  ...(count > 1 ? [buildCutLineSection(t)] : []),
73
73
  ...(hasPairs ? [buildDivLineSection(t)] : []),
74
74
  /* 띠를 쓰는 화면에서만 — 안 쓰는 표에 켜고 끌 것이 없는 절이 서면 안 된다 */
75
- ...(띠쓰나 ? [buildTopInfoSection(t), buildBottomInfoSection(t)] : []),
76
- ], [t, hasImage, hasColloquial, itemCount, hasPairs, isPaginated, count, 띠쓰나]);
75
+ ...(showBands ? [buildTopInfoSection(t), buildBottomInfoSection(t)] : []),
76
+ ], [t, hasImage, hasColloquial, itemCount, hasPairs, isPaginated, count, showBands]);
77
77
  const filterSelected = {
78
78
  ...buildTableSettingsSelected(t, { contentTypes, orientation, showOutline, cutLine, showDivLine, showTopInfo, showBottomInfo }),
79
79
  ...(itemCount > 0 ? { [t(hasPairs ? 'wordTable.filter.itemsPerPagePair' : 'wordTable.filter.itemsPerPageWord')]: `${effectiveItemsPerPage}개` } : {}),
80
- ...(isPaginated ? { [t('wordTable.filter.pageNumber', '페이지 번호')]: PAGE_NUM_POSITION_VALUE[pageNumberPosition] } : {}),
80
+ ...(isPaginated ? { [t('wordTable.filter.pageNumber', '페이지 번호')]: pageNumberPosition } : {}),
81
81
  };
82
82
  const handleSelect = useCallback((value, sectionLabel) => {
83
83
  // 기본 5개 설정은 공용 헬퍼가 처리 — 처리했으면 종료. 나머지(페이지당 개수·페이지 번호)만 여기서.
@@ -1,7 +1,7 @@
1
1
  import { compareKorean } from '../../utils/textSort';
2
2
  import { labelOfCellKey } from './wordTableReview';
3
3
  /** △ 를 반 점으로 치는 셈 — 저장할 때 서버가 쓰는 규칙과 같다. */
4
- function 점수(o, triangle, marked) {
4
+ function scoreOf(o, triangle, marked) {
5
5
  return marked === 0 ? 0 : (o + triangle * 0.5) / marked;
6
6
  }
7
7
  /**
@@ -18,16 +18,16 @@ function 점수(o, triangle, marked) {
18
18
  * 지금처럼 글자로 묶는다 — 그래서 같은 '배' 가 옛 기록 한 줄·번호 있는 기록 한 줄로 나뉘어 보일 수 있다.
19
19
  */
20
20
  export function weakItems(records) {
21
- const 모음 = new Map();
21
+ const byKey = new Map();
22
22
  /* 최근 것을 알려면 시간순으로 훑어야 한다. 서버는 최신순으로 주지만 여기서 다시
23
23
  정렬한다 — 부르는 쪽의 차례에 기대면 그쪽이 바뀔 때 조용히 어긋난다. */
24
- const 시간순 = [...records].sort((a, b) => a.scoredAt.localeCompare(b.scoredAt));
25
- for (const r of 시간순) {
24
+ const byTime = [...records].sort((a, b) => a.scoredAt.localeCompare(b.scoredAt));
25
+ for (const r of byTime) {
26
26
  for (const [cell, mark] of Object.entries(r.marks)) {
27
27
  const label = r.labels?.[cell];
28
28
  if (!label)
29
29
  continue;
30
- const cur = 모음.get(label) ?? { o: 0, triangle: 0, x: 0, latest: mark, latestAt: r.scoredAt };
30
+ const cur = byKey.get(label) ?? { o: 0, triangle: 0, x: 0, latest: mark, latestAt: r.scoredAt };
31
31
  if (mark === 'o')
32
32
  cur.o++;
33
33
  else if (mark === 'triangle')
@@ -38,13 +38,13 @@ export function weakItems(records) {
38
38
  continue;
39
39
  cur.latest = mark;
40
40
  cur.latestAt = r.scoredAt;
41
- 모음.set(label, cur);
41
+ byKey.set(label, cur);
42
42
  }
43
43
  }
44
44
  const out = [];
45
- for (const [key, v] of 모음) {
45
+ for (const [key, v] of byKey) {
46
46
  const tried = v.o + v.triangle + v.x;
47
- out.push({ label: labelOfCellKey(key), key, tried, o: v.o, triangle: v.triangle, x: v.x, rate: 점수(v.o, v.triangle, tried), latest: v.latest, latestAt: v.latestAt });
47
+ out.push({ label: labelOfCellKey(key), key, tried, o: v.o, triangle: v.triangle, x: v.x, rate: scoreOf(v.o, v.triangle, tried), latest: v.latest, latestAt: v.latestAt });
48
48
  }
49
49
  out.sort((a, b) => a.rate - b.rate || b.tried - a.tried || compareKorean(a.label, b.label) || compareKorean(a.key ?? a.label, b.key ?? b.label));
50
50
  return out;
@@ -56,27 +56,27 @@ export function weakItems(records) {
56
56
  * 0으로 두면 "제자리걸음"으로 읽혀, 아직 재 본 적 없는 것이 나쁜 소식처럼 보인다.
57
57
  */
58
58
  export function tableTrends(records) {
59
- const 모음 = new Map();
59
+ const byKey = new Map();
60
60
  for (const r of records) {
61
61
  /* 표를 지워도 이름으로는 이어 볼 수 있어야 한다 — id 가 없으면 이름을 열쇠로 쓴다. */
62
62
  const key = r.tableId ?? `name:${r.tableName}`;
63
- const cur = 모음.get(key) ?? { tableName: r.tableName, tableId: r.tableId, rows: [] };
63
+ const cur = byKey.get(key) ?? { tableName: r.tableName, tableId: r.tableId, rows: [] };
64
64
  cur.rows.push(r);
65
- 모음.set(key, cur);
65
+ byKey.set(key, cur);
66
66
  }
67
67
  const out = [];
68
- for (const v of 모음.values()) {
68
+ for (const v of byKey.values()) {
69
69
  /* 매긴 칸이 하나도 없는 기록은 흐름에서 뺀다 — 정답률 0%로 넣으면 [0, 0.5] 처럼 나아진 적 없는 표가 +50%로 읽혔다
70
70
  (summarize 는 같은 경우를 null 로 따로 둔다). 모두 비었으면 그 표는 흐름이 없다. */
71
- const 매긴수 = (r) => Object.values(r.marks).filter(m => m === 'o' || m === 'triangle' || m === 'x').length;
72
- const rows = v.rows.filter(r => 매긴수(r) > 0).sort((a, b) => a.scoredAt.localeCompare(b.scoredAt));
71
+ const markedCount = (r) => Object.values(r.marks).filter(m => m === 'o' || m === 'triangle' || m === 'x').length;
72
+ const rows = v.rows.filter(r => markedCount(r) > 0).sort((a, b) => a.scoredAt.localeCompare(b.scoredAt));
73
73
  if (rows.length === 0)
74
74
  continue;
75
75
  const rates = rows.map(r => {
76
76
  const vals = Object.values(r.marks);
77
77
  const o = vals.filter(m => m === 'o').length;
78
78
  const tri = vals.filter(m => m === 'triangle').length;
79
- return 점수(o, tri, 매긴수(r));
79
+ return scoreOf(o, tri, markedCount(r));
80
80
  });
81
81
  out.push({
82
82
  tableName: v.tableName,
@@ -18,10 +18,10 @@ export function sanitizeFileName(name) {
18
18
  return name.replace(/[\\/:*?"<>|]/g, '_').trim();
19
19
  }
20
20
  /** 아직 되돌리지 못한 탭 제목 되돌리기(인쇄가 끝나기 전 다음 인쇄가 올 때) */
21
- let 제목되돌리기 = null;
21
+ let restoreTitle = null;
22
22
  /* 진행 중인 인쇄 — 그림을 기다리는(최대 3초) 사이 다시 누르면 숨김 iframe 이 둘 생겨 인쇄 대화상자가 두 번 떴다(autoPrint 대기 중 '인쇄').
23
23
  afterprint 가 안 오는 브라우저에서 영영 막히지 않게 10초가 지나면 새 인쇄를 받는다. */
24
- let 인쇄중 = null;
24
+ let printing = null;
25
25
  /**
26
26
  * 캡처한 콘텐츠 엘리먼트들을 화면 밖 숨김 iframe에서 인쇄한다. 전역 @media print(모든
27
27
  * .word-table 노출)나 팝업 차단의 간섭 없이 넘긴 엘리먼트만 격리 인쇄한다. WordTableBlock의
@@ -31,14 +31,14 @@ let 인쇄중 = null;
31
31
  export function printWordTableElements(elements, paperW, paperH, fileName = '', header = '') {
32
32
  if (elements.length === 0)
33
33
  return;
34
- if (인쇄중 && 인쇄중.iframe.isConnected && Date.now() - 인쇄중.at < 10_000)
34
+ if (printing && printing.iframe.isConnected && Date.now() - printing.at < 10_000)
35
35
  return;
36
36
  const safeFileName = fileName ? sanitizeFileName(fileName) : '';
37
37
  // "PDF로 저장" 기본 파일명은 iframe이 아니라 최상위 탭(document)의 title에서 온다.
38
38
  /* 탭 제목은 **어느 쪽 afterprint 가 와도** 되돌린다 — 브라우저에 따라 iframe 인쇄의 print 이벤트가 그 프레임에만 가서,
39
39
  최상위에만 걸면 제목이 표 이름으로 남았고, 다음 인쇄가 그 표 이름을 '원래 제목' 으로 잡아 영영 안 돌아왔다.
40
40
  되돌리지 못한 앞 인쇄가 있으면 먼저 되돌린 뒤 원래 제목을 잡는다. */
41
- 제목되돌리기?.();
41
+ restoreTitle?.();
42
42
  let restore = null;
43
43
  if (safeFileName) {
44
44
  const prevTitle = document.title;
@@ -46,10 +46,10 @@ export function printWordTableElements(elements, paperW, paperH, fileName = '',
46
46
  restore = () => {
47
47
  document.title = prevTitle;
48
48
  window.removeEventListener('afterprint', restore);
49
- if (제목되돌리기 === restore)
50
- 제목되돌리기 = null;
49
+ if (restoreTitle === restore)
50
+ restoreTitle = null;
51
51
  };
52
- 제목되돌리기 = restore;
52
+ restoreTitle = restore;
53
53
  window.addEventListener('afterprint', restore);
54
54
  }
55
55
  const iframe = document.createElement('iframe');
@@ -60,14 +60,14 @@ export function printWordTableElements(elements, paperW, paperH, fileName = '',
60
60
  iframe.remove();
61
61
  return;
62
62
  }
63
- 인쇄중 = { iframe, at: Date.now() };
63
+ printing = { iframe, at: Date.now() };
64
64
  doc.open();
65
65
  doc.write(captureHtml(elements, paperW, paperH, true, safeFileName, header));
66
66
  doc.close();
67
67
  iframe.contentWindow?.addEventListener('afterprint', () => {
68
68
  restore?.();
69
- setTimeout(() => { iframe.remove(); if (인쇄중?.iframe === iframe)
70
- 인쇄중 = null; }, 0);
69
+ setTimeout(() => { iframe.remove(); if (printing?.iframe === iframe)
70
+ printing = null; }, 0);
71
71
  });
72
72
  }
73
73
  // 인쇄 시 PDF로 저장할 때 OS가 제안하는 기본 파일명은 문서 <title>에서 온다.
@@ -23,18 +23,18 @@ import { compareKorean } from '../../utils/textSort';
23
23
  */
24
24
  export function recommendTables(tables, weakLabels, limit = 5) {
25
25
  /* 약한 낱말을 Set 으로 — 표가 수십 개면 칸마다 배열을 훑는 비용이 쌓인다. */
26
- const 약한것 = new Set(weakLabels);
27
- if (약한것.size === 0)
26
+ const weakSet = new Set(weakLabels);
27
+ if (weakSet.size === 0)
28
28
  return [];
29
29
  const out = [];
30
30
  for (const t of tables) {
31
31
  /* **한 표 안에서 같은 낱말이 두 번 나와도 한 번으로 센다** — 대립쌍표에는 같은 쌍이
32
32
  거듭 나오는 일이 있고, 그걸 두 번 세면 실속 없는 표가 위로 올라온다. */
33
- const 품은것 = new Set();
33
+ const coveredSet = new Set();
34
34
  const covered = [];
35
35
  for (const label of t.cellLabels) {
36
- if (약한것.has(label) && !품은것.has(label)) {
37
- 품은것.add(label);
36
+ if (weakSet.has(label) && !coveredSet.has(label)) {
37
+ coveredSet.add(label);
38
38
  covered.push(label);
39
39
  }
40
40
  }
@@ -57,8 +57,8 @@ export function recommendTables(tables, weakLabels, limit = 5) {
57
57
  * 고를 때 또 헷갈린다.
58
58
  */
59
59
  export function uncoveredLabels(recommendations, weakLabels) {
60
- const 덮인것 = new Set(recommendations.flatMap(r => r.covered));
61
- return weakLabels.filter(l => !덮인것.has(l));
60
+ const alreadyCovered = new Set(recommendations.flatMap(r => r.covered));
61
+ return weakLabels.filter(l => !alreadyCovered.has(l));
62
62
  }
63
63
  /**
64
64
  * 새 표에 담을 **칸**을 가진 표들에서 찾아온다.
@@ -74,17 +74,17 @@ export function uncoveredLabels(recommendations, weakLabels) {
74
74
  * 표를 훑은 차례로 담으면 그 순서가 흐트러진다.
75
75
  */
76
76
  export function cellsForLabels(tables, labels) {
77
- const 찾은것 = new Map();
77
+ const found = new Map();
78
78
  for (const t of tables) {
79
79
  for (const cell of t.cells) {
80
80
  const label = t.labelOf(cell);
81
- if (!찾은것.has(label))
82
- 찾은것.set(label, cell);
81
+ if (!found.has(label))
82
+ found.set(label, cell);
83
83
  }
84
84
  }
85
85
  const out = [];
86
86
  for (const label of labels) {
87
- const cell = 찾은것.get(label);
87
+ const cell = found.get(label);
88
88
  if (cell !== undefined)
89
89
  out.push(cell);
90
90
  }
@@ -9,7 +9,7 @@ import { toKstYmd, formatYmd } from '../../utils/date';
9
9
  * 이 파일에는 화면도 저장도 없다. 무엇을 담을지만 안다.
10
10
  */
11
11
  /** 다음 표에 담을 표시 — 틀림과 애매. */
12
- const 담을것 = new Set(['x', 'triangle']);
12
+ const COUNTED_MARKS = new Set(['x', 'triangle']);
13
13
  /**
14
14
  * 다음 표에 담을 칸들.
15
15
  *
@@ -28,29 +28,29 @@ export function reviewCells(items, marks) {
28
28
  const out = [];
29
29
  for (let i = 0; i < items.length; i++) {
30
30
  const m = marks[String(i)];
31
- if (m && 담을것.has(m))
31
+ if (m && COUNTED_MARKS.has(m))
32
32
  out.push(items[i]);
33
33
  }
34
34
  return out;
35
35
  }
36
36
  /** 복습 꼬리 — `[N차 ]복습 MM.DD`. '복습' 뒤에 날짜가 따라와야만 꼬리로 본다("복습 자료" 같은 이름을 잘라 먹지 않게). */
37
- const 복습꼬리 = /\s*(?:(\d+)차\s*)?복습\s*\d{2}\.\d{2}$/;
37
+ const REVIEW_SUFFIX = /\s*(?:(\d+)차\s*)?복습\s*\d{2}\.\d{2}$/;
38
38
  /** 원래 표 → (꼬리를 뗀 바탕 이름, 새 복습표의 회차). 메타가 있으면 메타를, 없으면 이름을 읽는다. */
39
- function 바탕과회차(src) {
40
- const 이름 = src.name.trim();
41
- const 메타회차 = src.reviewRound;
42
- if (typeof 메타회차 === 'number' && Number.isFinite(메타회차) && 메타회차 >= 0) {
43
- const 지난 = Math.floor(메타회차);
44
- if (지난 === 0)
45
- return { 바탕: 이름, 회차: 1 };
39
+ function baseAndRound(src) {
40
+ const name = src.name.trim();
41
+ const metaRound = src.reviewRound;
42
+ if (typeof metaRound === 'number' && Number.isFinite(metaRound) && metaRound >= 0) {
43
+ const past = Math.floor(metaRound);
44
+ if (past === 0)
45
+ return { base: name, round: 1 };
46
46
  /* 메타가 말하는 **그 회차의 꼬리 하나만** 뗀다 — `수업 3차 복습 09.15`(1회) 에서 `3차` 를 회차로 먹지 않는다.
47
47
  N차는 앞이 공백·처음일 때만 — `수업 13차 복습`(3회) 의 `3차` 를 떼지 않게. 사람이 이름을 고쳐 꼬리가 없으면
48
48
  이름을 통째로 바탕으로 둔다. */
49
- const 그꼬리 = 지난 === 1
49
+ const suffix = past === 1
50
50
  ? /\s*복습\s*\d{2}\.\d{2}$/
51
- : new RegExp(`(?:^|\\s)${지난}차\\s*복습\\s*\\d{2}\\.\\d{2}$`);
52
- const m = 그꼬리.exec(이름);
53
- return { 바탕: m ? 이름.slice(0, m.index).trim() : 이름, 회차: 지난 + 1 };
51
+ : new RegExp(`(?:^|\\s)${past}차\\s*복습\\s*\\d{2}\\.\\d{2}$`);
52
+ const m = suffix.exec(name);
53
+ return { base: m ? name.slice(0, m.index).trim() : name, round: past + 1 };
54
54
  }
55
55
  /* **메타 없는 옛 표 — 이름을 파싱한다.** 복습표를 또 복습하면 꼬리가 쌓인다. 그대로 두면
56
56
  `검수 대립쌍 복습 08.29 복습 08.30 복습 09.02` 처럼 자란다 — 복습을 거듭하는 것은
@@ -63,16 +63,16 @@ function 바탕과회차(src) {
63
63
 
64
64
  '복습' 은 뒤에 `MM.DD` 가 따라와야만 꼬리로 본다 — "복습 자료" 같은 사람이 지은
65
65
  이름을 잘라 먹지 않으려는 것이다. */
66
- let 바탕 = 이름;
67
- let 세대 = 1;
66
+ let base = name;
67
+ let generation = 1;
68
68
  for (;;) {
69
- const m = 복습꼬리.exec(바탕);
69
+ const m = REVIEW_SUFFIX.exec(base);
70
70
  if (!m)
71
71
  break;
72
- 세대 = Math.max(세대, (m[1] ? Number(m[1]) : 1) + 1);
73
- 바탕 = 바탕.slice(0, m.index).trim();
72
+ generation = Math.max(generation, (m[1] ? Number(m[1]) : 1) + 1);
73
+ base = base.slice(0, m.index).trim();
74
74
  }
75
- return { 바탕, 회차: 세대 };
75
+ return { base, round: generation };
76
76
  }
77
77
  /**
78
78
  * 복습표 이름.
@@ -89,11 +89,11 @@ export function reviewTableName(original, today) {
89
89
  /* 지역 날짜가 아니라 KST 로 읽는다 — 이 날짜는 **표 이름에 저장되고** 같은 이름끼리
90
90
  겹치는지 세는 데 쓰인다. 기기 시간대를 따르면 같은 날 만든 표가 다른 날짜를 달고
91
91
  남아 회차 세기가 어긋난다. */
92
- const [, 월, 일] = toKstYmd(today).split('-');
93
- const 날짜 = `${월}.${일}`;
94
- const { 바탕, 회차: 세대 } = 바탕과회차(typeof original === 'string' ? { name: original } : original);
95
- const 회차 = 세대 >= 2 ? `${세대}차 ` : '';
96
- return `${바탕 || '단어표'} ${회차}복습 ${날짜}`;
92
+ const [, month, day] = toKstYmd(today).split('-');
93
+ const dateLabel = `${month}.${day}`;
94
+ const { base, round: generation } = baseAndRound(typeof original === 'string' ? { name: original } : original);
95
+ const round = generation >= 2 ? `${generation}차 ` : '';
96
+ return `${base || '단어표'} ${round}복습 ${dateLabel}`;
97
97
  }
98
98
  /**
99
99
  * 새로 만들 복습표에 **함께 저장할 메타** — 맨 처음 원본 표 id 와 이번이 몇 번째 복습인지.
@@ -106,7 +106,7 @@ export function reviewTableName(original, today) {
106
106
  export function reviewMeta(source) {
107
107
  return {
108
108
  reviewOf: source.reviewOf ?? source.id ?? undefined,
109
- reviewRound: 바탕과회차(source).회차,
109
+ reviewRound: baseAndRound(source).round,
110
110
  };
111
111
  }
112
112
  /**
@@ -165,6 +165,6 @@ export function labelOfCellKey(key) {
165
165
  */
166
166
  export function scoreSheetHeader(customerName, tableName, today) {
167
167
  /* 인쇄물에 찍히는 날짜다 — 치료실의 그날(KST)이어야 한다. */
168
- const 날짜 = formatYmd(today);
169
- return [customerName.trim(), 날짜, tableName.trim()].filter(Boolean).join(' · ');
168
+ const dateLabel = formatYmd(today);
169
+ return [customerName.trim(), dateLabel, tableName.trim()].filter(Boolean).join(' · ');
170
170
  }
@@ -1,14 +1,6 @@
1
1
  import type { TFunction } from 'i18next';
2
2
  import { type FilterPanelSection } from '../FilterPanel';
3
3
  import type { WordTableOrientation, WordTableContentType, WordTableCutLine, PageNumberPosition } from '../WordTable';
4
- /**
5
- * '표 설정'(단어표 표시 옵션) 공용 섹션 빌더·선택상태·핸들러.
6
- * 표시 전용 훅(useWordTableDisplaySettings)과 편집/페이지분할까지 있는 WordTableBlock이 기본 5개 설정
7
- * (구성·용지 방향·외곽선·절취선·구분선)을 여기서 공용으로 조립한다. 각 소비처는 자신의 표시 조건
8
- * (절취선=칸>1, 구분선=대립쌍 등)에 따라 섹션 포함 여부만 결정하고, WordTableBlock은 칸 수·페이지당
9
- * 개수·페이지 번호 섹션을 그 사이에 끼워 넣는다.
10
- */
11
- /** @public 구성(이미지/단어/표준발음/구어체발음) 다중 토글 섹션. */
12
4
  export declare function buildCompositionSection(t: TFunction, opts: {
13
5
  hasImage: boolean;
14
6
  hasColloquial: boolean;
@@ -41,9 +33,14 @@ export declare function buildItemsPerPageSection(t: TFunction, opts: {
41
33
  export declare function itemsPerPageFromChoice(n: number, itemCount: number): number;
42
34
  /** @public 페이지 번호 위치(왼쪽/중앙/오른쪽/미표시) 세그먼트 섹션 — 실제 2페이지 이상일 때만 노출. */
43
35
  export declare function buildPageNumberSection(t: TFunction): FilterPanelSection;
44
- /** @public 페이지 번호 위치 → filterSelected 표시값. */
45
- export declare const PAGE_NUM_POSITION_VALUE: Record<PageNumberPosition, string>;
46
- /** @public FilterPanel 선택값 → 페이지 번호 위치. */
36
+ /**
37
+ * @public FilterPanel 선택값 → 페이지 번호 위치.
38
+ *
39
+ * **섹션의 `value` 가 곧 코드다.** 전에는 `value` 가 한글(`'왼쪽'`)이라 여기서 되돌리는 표
40
+ * (`PAGE_NUM_POSITION_VALUE`)와 파서가 **둘 다** 필요했다 — 값을 코드로 돌리니 그 둘이
41
+ * 항등식이 되어 표를 걷었다(2026-10-13). *옮기는 표가 사라지는 것이 옳게 옮긴 표시다.*
42
+ * 모르는 값은 `'none'` 으로 떨어뜨린다(옛 한글 값이 어디선가 오더라도 번호를 안 찍는다).
43
+ */
47
44
  export declare function parsePageNumberPosition(value: string): PageNumberPosition;
48
45
  /** @public 기본 5개 설정의 filterSelected 엔트리(호출부가 자신의 추가 섹션과 spread). */
49
46
  export declare function buildTableSettingsSelected(t: TFunction, v: {