@ihabdevteam/core 0.321.0 → 0.323.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 (217) hide show
  1. package/CHANGELOG.md +617 -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 +108 -66
  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/HearingDeviceInfo.d.ts +5 -5
  24. package/dist/packages/core/src/components/HearingDeviceInfo.js +2 -2
  25. package/dist/packages/core/src/components/Icon.js +2 -2
  26. package/dist/packages/core/src/components/ItemActions.js +16 -16
  27. package/dist/packages/core/src/components/LandingHeader.js +6 -6
  28. package/dist/packages/core/src/components/LineChart.js +24 -24
  29. package/dist/packages/core/src/components/Loader.js +5 -5
  30. package/dist/packages/core/src/components/NoticeBox.js +3 -3
  31. package/dist/packages/core/src/components/PageTitle.js +2 -2
  32. package/dist/packages/core/src/components/Pagination.js +4 -4
  33. package/dist/packages/core/src/components/Panel.js +2 -2
  34. package/dist/packages/core/src/components/PanelRow.d.ts +2 -4
  35. package/dist/packages/core/src/components/PanelRow.js +8 -15
  36. package/dist/packages/core/src/components/PassCard/PassCard.js +9 -9
  37. package/dist/packages/core/src/components/PassCard/passCardProduct.js +2 -2
  38. package/dist/packages/core/src/components/PaymentPlan.js +5 -5
  39. package/dist/packages/core/src/components/Popup/ConfirmPopup.js +29 -29
  40. package/dist/packages/core/src/components/Popup/CouponInputPopup.js +2 -2
  41. package/dist/packages/core/src/components/Popup/DeviceListPopup.d.ts +4 -8
  42. package/dist/packages/core/src/components/Popup/DeviceListPopup.js +1 -1
  43. package/dist/packages/core/src/components/Popup/DeviceSideTable.js +19 -26
  44. package/dist/packages/core/src/components/Popup/ListPopup.js +6 -6
  45. package/dist/packages/core/src/components/Popup/LoadTablePopup.js +10 -10
  46. package/dist/packages/core/src/components/Popup/Popup.js +27 -27
  47. package/dist/packages/core/src/components/Popup/SaveTablePopup.js +5 -5
  48. package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +4 -4
  49. package/dist/packages/core/src/components/Popup/customerDetailUtils.js +18 -18
  50. package/dist/packages/core/src/components/Popup/useDiscardGuard.js +4 -4
  51. package/dist/packages/core/src/components/ProgressIndicator.js +5 -5
  52. package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +16 -16
  53. package/dist/packages/core/src/components/Rating.js +20 -20
  54. package/dist/packages/core/src/components/Report/ReportDocument.js +21 -21
  55. package/dist/packages/core/src/components/RingChart.js +5 -5
  56. package/dist/packages/core/src/components/SectionHeadFloat.js +15 -15
  57. package/dist/packages/core/src/components/SegmentedControl.js +34 -34
  58. package/dist/packages/core/src/components/SideNav.js +37 -37
  59. package/dist/packages/core/src/components/Slider.js +2 -2
  60. package/dist/packages/core/src/components/SoundCompass.js +5 -5
  61. package/dist/packages/core/src/components/Table.js +257 -98
  62. package/dist/packages/core/src/components/Tabs.js +12 -12
  63. package/dist/packages/core/src/components/TextField.js +34 -34
  64. package/dist/packages/core/src/components/Toast.js +6 -6
  65. package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +4 -4
  66. package/dist/packages/core/src/components/Tooltip/Tooltip.js +22 -22
  67. package/dist/packages/core/src/components/Tooltip/TrainingTooltip.js +4 -4
  68. package/dist/packages/core/src/components/TopBanner.js +28 -28
  69. package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +11 -11
  70. package/dist/packages/core/src/components/Training/CategoryIcon.js +2 -2
  71. package/dist/packages/core/src/components/Training/NoisePlayerContext.d.ts +7 -4
  72. package/dist/packages/core/src/components/Training/NoisePlayerContext.js +12 -12
  73. package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +2 -2
  74. package/dist/packages/core/src/components/Training/TrainingBlock.js +12 -12
  75. package/dist/packages/core/src/components/Training/TrainingBlockButton.js +10 -10
  76. package/dist/packages/core/src/components/Training/TrainingBlockGrid.js +21 -21
  77. package/dist/packages/core/src/components/Training/TrainingCard.css +13 -1
  78. package/dist/packages/core/src/components/Training/TrainingCard.js +2 -2
  79. package/dist/packages/core/src/components/Training/TrainingCardGroup.js +6 -6
  80. package/dist/packages/core/src/components/Training/TrainingPagePopup.js +83 -83
  81. package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +17 -17
  82. package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +128 -128
  83. package/dist/packages/core/src/components/Training/TrainingStepCard.js +18 -18
  84. package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +4 -4
  85. package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +12 -12
  86. package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +13 -13
  87. package/dist/packages/core/src/components/Training/TrainingStepTitle.js +4 -4
  88. package/dist/packages/core/src/components/Training/TrainingWordCard.js +17 -17
  89. package/dist/packages/core/src/components/Training/WordTablePopup.js +5 -5
  90. package/dist/packages/core/src/components/Training/audioPlaybackRules.js +8 -8
  91. package/dist/packages/core/src/components/Training/audioTimeline.js +25 -25
  92. package/dist/packages/core/src/components/Training/oneShotSound.d.ts +1 -1
  93. package/dist/packages/core/src/components/Training/oneShotSound.js +14 -14
  94. package/dist/packages/core/src/components/Training/stepAnswerable.d.ts +2 -2
  95. package/dist/packages/core/src/components/Training/trainingMissStats.js +22 -22
  96. package/dist/packages/core/src/components/Training/trainingPopupUtils.js +7 -7
  97. package/dist/packages/core/src/components/Training/useAudioPreview.js +15 -15
  98. package/dist/packages/core/src/components/Training/useFloatingFocus.js +14 -14
  99. package/dist/packages/core/src/components/Training/useNoiseFade.js +22 -22
  100. package/dist/packages/core/src/components/Training/useObjectUrls.js +2 -2
  101. package/dist/packages/core/src/components/Training/usePlaybackSequencer.d.ts +2 -2
  102. package/dist/packages/core/src/components/Training/usePlaybackSequencer.js +23 -23
  103. package/dist/packages/core/src/components/Training/useProgressTicker.js +33 -33
  104. package/dist/packages/core/src/components/Training/useTTSPlayback.js +17 -17
  105. package/dist/packages/core/src/components/Training/useWaveReady.js +11 -11
  106. package/dist/packages/core/src/components/UpdateAlert.js +6 -6
  107. package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +7 -1
  108. package/dist/packages/core/src/components/Word/TableFilterPanel.js +3 -3
  109. package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +18 -18
  110. package/dist/packages/core/src/components/Word/WordTableBlock.js +28 -28
  111. package/dist/packages/core/src/components/Word/WordTableButton.css +16 -2
  112. package/dist/packages/core/src/components/Word/WordTableThumbnail.css +6 -1
  113. package/dist/packages/core/src/components/Word/generatedDraft.js +14 -14
  114. package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -7
  115. package/dist/packages/core/src/components/Word/wordScoreStats.js +17 -17
  116. package/dist/packages/core/src/components/Word/wordTablePrint.js +10 -10
  117. package/dist/packages/core/src/components/Word/wordTableRecommend.js +13 -13
  118. package/dist/packages/core/src/components/Word/wordTableReview.js +28 -28
  119. package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +8 -11
  120. package/dist/packages/core/src/components/Word/wordTableSettings.js +48 -36
  121. package/dist/packages/core/src/components/WordTable.js +130 -130
  122. package/dist/packages/core/src/components/barChartData.d.ts +16 -1
  123. package/dist/packages/core/src/components/barChartData.js +49 -29
  124. package/dist/packages/core/src/components/barChartTypes.js +8 -8
  125. package/dist/packages/core/src/components/tableTrackList.js +11 -11
  126. package/dist/packages/core/src/components/updateAlertStack.js +28 -28
  127. package/dist/packages/core/src/components/useBarChartControl.js +4 -6
  128. package/dist/packages/core/src/components/useComboboxSuggestions.js +17 -17
  129. package/dist/packages/core/src/components/useToastLifecycle.js +18 -18
  130. package/dist/packages/core/src/components/wordTableLayout.d.ts +22 -7
  131. package/dist/packages/core/src/components/wordTableLayout.js +5 -5
  132. package/dist/packages/core/src/context/I18nProvider.js +6 -6
  133. package/dist/packages/core/src/hooks/focusReturn.js +27 -27
  134. package/dist/packages/core/src/hooks/inputModality.d.ts +1 -1
  135. package/dist/packages/core/src/hooks/inputModality.js +14 -14
  136. package/dist/packages/core/src/hooks/touchToggle.js +39 -39
  137. package/dist/packages/core/src/hooks/useAnchoredMenu.js +30 -30
  138. package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +28 -28
  139. package/dist/packages/core/src/hooks/useDeviceManagement.d.ts +5 -17
  140. package/dist/packages/core/src/hooks/useDeviceManagement.js +20 -8
  141. package/dist/packages/core/src/hooks/useDocumentTitle.js +10 -10
  142. package/dist/packages/core/src/hooks/useEditableList.js +4 -4
  143. package/dist/packages/core/src/hooks/useFocusTrap.js +2 -2
  144. package/dist/packages/core/src/hooks/useFontScale.js +6 -6
  145. package/dist/packages/core/src/hooks/useIcon.js +7 -7
  146. package/dist/packages/core/src/hooks/useListControls.js +2 -2
  147. package/dist/packages/core/src/hooks/usePageResetOnChange.d.ts +3 -3
  148. package/dist/packages/core/src/hooks/usePageResetOnChange.js +6 -6
  149. package/dist/packages/core/src/hooks/usePasswordChange.js +4 -4
  150. package/dist/packages/core/src/hooks/usePendingPress.js +7 -7
  151. package/dist/packages/core/src/hooks/useRelativeDay.js +4 -4
  152. package/dist/packages/core/src/hooks/useSoundCompassExam.js +6 -6
  153. package/dist/packages/core/src/hooks/useTablePaging.d.ts +7 -7
  154. package/dist/packages/core/src/hooks/useTablePaging.js +40 -34
  155. package/dist/packages/core/src/hooks/useTrainingSessionLog.js +35 -35
  156. package/dist/packages/core/src/hooks/useVisitSchedule.js +4 -4
  157. package/dist/packages/core/src/hooks/useWordAttributeFilter.js +14 -14
  158. package/dist/packages/core/src/hooks/useWordMeanings.js +5 -5
  159. package/dist/packages/core/src/hooks/useWordTrainingFilter.d.ts +3 -1
  160. package/dist/packages/core/src/hooks/useWordTrainingFilter.js +14 -12
  161. package/dist/packages/core/src/i18n/locales/en.json +13 -2
  162. package/dist/packages/core/src/i18n/locales/ko.json +12 -1
  163. package/dist/packages/core/src/types/wordSearch.d.ts +9 -0
  164. package/dist/packages/core/src/utils/audioMeter.js +5 -5
  165. package/dist/packages/core/src/utils/audioPeaks.d.ts +1 -1
  166. package/dist/packages/core/src/utils/audioPeaks.js +18 -18
  167. package/dist/packages/core/src/utils/blockGrid.js +4 -4
  168. package/dist/packages/core/src/utils/chunkSkew.js +9 -9
  169. package/dist/packages/core/src/utils/clipboard.js +3 -3
  170. package/dist/packages/core/src/utils/dailySelectionUtils.js +12 -12
  171. package/dist/packages/core/src/utils/date.js +12 -12
  172. package/dist/packages/core/src/utils/devWarn.d.ts +2 -2
  173. package/dist/packages/core/src/utils/devWarn.js +7 -7
  174. package/dist/packages/core/src/utils/deviceKind.d.ts +37 -7
  175. package/dist/packages/core/src/utils/deviceKind.js +53 -10
  176. package/dist/packages/core/src/utils/errorMessage.js +7 -7
  177. package/dist/packages/core/src/utils/filterPanelSummary.d.ts +1 -1
  178. package/dist/packages/core/src/utils/filterPanelSummary.js +16 -16
  179. package/dist/packages/core/src/utils/format.js +4 -4
  180. package/dist/packages/core/src/utils/getTrainingData.js +9 -9
  181. package/dist/packages/core/src/utils/imageConfig.js +17 -17
  182. package/dist/packages/core/src/utils/koreanSyllable.js +18 -18
  183. package/dist/packages/core/src/utils/navScroll.d.ts +2 -2
  184. package/dist/packages/core/src/utils/navScroll.js +6 -6
  185. package/dist/packages/core/src/utils/phonemeConvert.d.ts +7 -0
  186. package/dist/packages/core/src/utils/phonemeConvert.js +19 -10
  187. package/dist/packages/core/src/utils/phonemeGroups.js +2 -2
  188. package/dist/packages/core/src/utils/phonemePairSearch.d.ts +1 -0
  189. package/dist/packages/core/src/utils/phonemePairSearch.js +118 -102
  190. package/dist/packages/core/src/utils/phonemeSectionCodes.d.ts +52 -0
  191. package/dist/packages/core/src/utils/phonemeSectionCodes.js +76 -0
  192. package/dist/packages/core/src/utils/prefetchChunk.js +6 -6
  193. package/dist/packages/core/src/utils/pressable.d.ts +1 -1
  194. package/dist/packages/core/src/utils/pressable.js +3 -3
  195. package/dist/packages/core/src/utils/safeHref.js +4 -4
  196. package/dist/packages/core/src/utils/savedWordTables.js +5 -5
  197. package/dist/packages/core/src/utils/sessionTab.d.ts +7 -7
  198. package/dist/packages/core/src/utils/sessionTab.js +9 -9
  199. package/dist/packages/core/src/utils/speak.js +12 -12
  200. package/dist/packages/core/src/utils/textSort.d.ts +5 -5
  201. package/dist/packages/core/src/utils/textSort.js +47 -47
  202. package/dist/packages/core/src/utils/theme.js +3 -3
  203. package/dist/packages/core/src/utils/trainingItemResults.js +3 -3
  204. package/dist/packages/core/src/utils/ttsConfig.js +2 -2
  205. package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +2 -2
  206. package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +48 -48
  207. package/dist/packages/core/src/utils/wordContrast.js +3 -3
  208. package/dist/packages/core/src/utils/wordPairIndex.js +4 -4
  209. package/dist/packages/core/src/utils/wordTrainingGenerators.js +4 -4
  210. package/dist/packages/core/src/utils/wordTrainingPool.js +15 -15
  211. package/dist/packages/core/src/utils/wordTrainingRuntime.js +6 -6
  212. package/dist/packages/core/src/utils/wornDate.d.ts +3 -3
  213. package/dist/packages/core/src/utils/wornDate.js +14 -14
  214. package/dist/packages/core/src/version.d.ts +1 -1
  215. package/dist/packages/core/src/version.js +1 -1
  216. package/dist/utils.cjs.js +4 -4
  217. package/package.json +1 -1
@@ -12,7 +12,7 @@ import Popup from '../Popup/Popup';
12
12
  import WordTable, { WORD_TABLE_PAPER } from '../WordTable';
13
13
  import { chunkItems } from '../wordTableLayout';
14
14
  import TableFilterPanel from './TableFilterPanel';
15
- import { buildItemsPerPageSection, buildPageNumberSection, PAGE_NUM_POSITION_VALUE, parsePageNumberPosition, itemsPerPageFromChoice } from './wordTableSettings';
15
+ import { buildItemsPerPageSection, buildPageNumberSection, parsePageNumberPosition, itemsPerPageFromChoice } from './wordTableSettings';
16
16
  import Toast from '../Toast';
17
17
  import { useToastState } from '../../hooks/useToastState';
18
18
  import { getImageResolver, checkImageExists } from '../../utils/imageConfig';
@@ -24,24 +24,24 @@ import { CELL_COUNTS } from './wordTableBlockTypes';
24
24
  // ── 컴포넌트 ───────────────────────────────────────────────
25
25
  /** @public */
26
26
  /** 편집 칸의 낱말 → 저장 항목의 낱말. 편집 칸에서 보이던 구어체 발음·등급·품사·유형을 그대로 옮긴다. */
27
- function 항목낱말(e) {
27
+ function toResultWord(e) {
28
28
  return {
29
29
  label: e.word, subLabel: e.pronunciation, colloquialLabel: e.colloquialPronunciation, homonum: e.homonum,
30
30
  grade: e.grade, syllables: [...e.word].length, pos: e.pos, wordType: e.wordType,
31
31
  };
32
32
  }
33
33
  /** 동음이의어 번호까지 넣은 낱말들로 만든 id — 첫 낱말만 쓰면 '배'(1)·'배'(2), '가다|나다'·'가다|다다' 가 같은 id 였다 */
34
- function 항목id(entries) {
34
+ function itemId(entries) {
35
35
  return entries.map(e => (e.homonum != null ? `${e.word}#${e.homonum}` : e.word)).join('|');
36
36
  }
37
37
  /** 표 안에서 같은 낱말을 두 칸에 넣었을 때도 id 가 겹치지 않게 뒤에 ~2, ~3 을 붙인다 */
38
- function 겹치지않는id(list) {
39
- const 쓴 = new Set();
38
+ function withUniqueIds(list) {
39
+ const used = new Set();
40
40
  return list.map(it => {
41
41
  let id = it.id;
42
- for (let n = 2; 쓴.has(id); n++)
42
+ for (let n = 2; used.has(id); n++)
43
43
  id = `${it.id}~${n}`;
44
- 쓴.add(id);
44
+ used.add(id);
45
45
  return id === it.id ? it : { ...it, id };
46
46
  });
47
47
  }
@@ -78,14 +78,14 @@ export default function WordTableBlock({ title, titleIcon, disc, filterTitle, fi
78
78
  const visibleCells = cells.slice(0, cellCount);
79
79
  /* 띠를 켠 표면 설정 판에 '상단 정보'·'하단 정보' 고르개가 선다 — 스위치를 따로 두지
80
80
  않는다(부르는 쪽이 둘을 어긋나게 줄 일이 없게). 고른 값은 `brand.top`·`bottom` 을 덮는다. */
81
- const 띠쓰나 = Boolean(brand?.show);
81
+ const showBand = Boolean(brand?.show);
82
82
  const [showTopInfo, setShowTopInfo] = useState(true);
83
83
  const [showBottomInfo, setShowBottomInfo] = useState(true);
84
- const 띠 = brand && { ...brand, top: showTopInfo, bottom: showBottomInfo };
84
+ const band = brand && { ...brand, top: showTopInfo, bottom: showBottomInfo };
85
85
  // 공통 단어 표 설정 상태
86
86
  const [orientation, setOrientation] = useState('landscape');
87
- const [contentTypes, setContentTypes] = useState(['이미지', '단어', '표준 발음']);
88
- const [cutLine, setCutLine] = useState('표시');
87
+ const [contentTypes, setContentTypes] = useState(['image', 'word', 'standardPron']);
88
+ const [cutLine, setCutLine] = useState('show');
89
89
  const [showOutline, setShowOutline] = useState(true);
90
90
  const [showDivLine, setShowDivLine] = useState(true);
91
91
  // 인쇄 캡처용 그리드 콘텐츠 ref (단일 페이지)
@@ -209,7 +209,7 @@ export default function WordTableBlock({ title, titleIcon, disc, filterTitle, fi
209
209
  const middleSelected = {
210
210
  ...((isEditable || showCellCount) ? { [t('wordTable.filter.columns', '칸 수')]: `${count}칸` } : {}),
211
211
  ...(pagesEnabled && (items?.length ?? 0) > 0 ? { [t(hasPairs ? 'wordTable.filter.itemsPerPagePair' : 'wordTable.filter.itemsPerPageWord')]: `${effectiveItemsPerPage}개` } : {}),
212
- ...(isPaginated ? { [t('wordTable.filter.pageNumber', '페이지 번호')]: PAGE_NUM_POSITION_VALUE[pageNumberPosition] } : {}),
212
+ ...(isPaginated ? { [t('wordTable.filter.pageNumber', '페이지 번호')]: pageNumberPosition } : {}),
213
213
  };
214
214
  const handleMiddleSelect = (value, sectionLabel) => {
215
215
  if (sectionLabel === t('wordTable.filter.columns', '칸 수')) {
@@ -229,14 +229,14 @@ export default function WordTableBlock({ title, titleIcon, disc, filterTitle, fi
229
229
  };
230
230
  // 저장 팝업용 현재 items / preview
231
231
  const saveItems = isEditable
232
- ? 겹치지않는id(visibleCells.filter((c) => c !== null && c.length > 0).map(c => ({
233
- id: 항목id(c),
234
- words: c.map(항목낱말),
232
+ ? withUniqueIds(visibleCells.filter((c) => c !== null && c.length > 0).map(c => ({
233
+ id: itemId(c),
234
+ words: c.map(toResultWord),
235
235
  })))
236
236
  : items ?? [];
237
237
  /* 저장한 뒤에는 **숨은 칸(cellCount 뒤)을 비운다** — 2칸으로 줄여 저장한 표를 4칸으로 늘리면 저장본에 없던
238
238
  셋째·넷째 낱말이 다시 떠, 저장한 것과 화면이 달랐다. 저장하지 않고 줄였다 늘리는 동안에는 그대로 들고 있는다. */
239
- const 숨은칸비우기 = () => {
239
+ const clearHiddenCells = () => {
240
240
  if (isEditable)
241
241
  setCells(prev => (prev.length > cellCount ? prev.slice(0, cellCount) : prev));
242
242
  };
@@ -250,13 +250,13 @@ export default function WordTableBlock({ title, titleIcon, disc, filterTitle, fi
250
250
  if (savedLower)
251
251
  setSavedNames(prev => prev.includes(savedLower) ? prev : [...prev, savedLower]);
252
252
  onSaveComplete(trimmedName, saveItems, trimmedDescription || undefined, asNew);
253
- 숨은칸비우기();
253
+ clearHiddenCells();
254
254
  return;
255
255
  }
256
256
  // saveOnly: 이름 입력 팝업 없이 바로 저장(이름은 상위에서 처리)
257
257
  if (saveOnly && onSaveComplete) {
258
258
  onSaveComplete('', saveItems, undefined, asNew);
259
- 숨은칸비우기();
259
+ clearHiddenCells();
260
260
  return;
261
261
  }
262
262
  if (onSaveComplete) {
@@ -279,7 +279,7 @@ export default function WordTableBlock({ title, titleIcon, disc, filterTitle, fi
279
279
  if (!saveAsTrimmed || saveAsCollides || !onSaveComplete)
280
280
  return;
281
281
  onSaveComplete(saveAsTrimmed, saveItems, trimmedDescription || undefined, true);
282
- 숨은칸비우기();
282
+ clearHiddenCells();
283
283
  setShowSaveAsPopup(false);
284
284
  };
285
285
  // 저장 버튼 활성 조건 — 인라인 모드에선 이름 필수·중복(others) 불가. 항목이 비어도 불가.
@@ -307,7 +307,7 @@ export default function WordTableBlock({ title, titleIcon, disc, filterTitle, fi
307
307
  items: saveItems,
308
308
  existingNames: getSavedTables().map(tb => tb.name),
309
309
  saveAsNew,
310
- onSave: (name, description) => { onSaveComplete(name, saveItems, description, saveAsNew); 숨은칸비우기(); setShowSavePopup(false); },
310
+ onSave: (name, description) => { onSaveComplete(name, saveItems, description, saveAsNew); clearHiddenCells(); setShowSavePopup(false); },
311
311
  onCancel: () => setShowSavePopup(false),
312
312
  }), (isEditable || onAddItem) && activeCell !== null && renderWordSearch?.({
313
313
  cellIndex: activeCell,
@@ -316,25 +316,25 @@ export default function WordTableBlock({ title, titleIcon, disc, filterTitle, fi
316
316
  const entry = entries[0];
317
317
  if (entry) {
318
318
  // 이미 있는 항목과 id 가 겹치면(같은 낱말의 다른 동음이의어·같은 낱말) onRemoveItem(id) 가 둘을 함께 지운다
319
- const 쓴id = new Set((items ?? []).map(it => it.id));
320
- let id = 항목id([entry]);
321
- for (let n = 2; 쓴id.has(id); n++)
322
- id = `${항목id([entry])}~${n}`;
323
- onAddItem({ id, words: [항목낱말(entry)] });
319
+ const usedIds = new Set((items ?? []).map(it => it.id));
320
+ let id = itemId([entry]);
321
+ for (let n = 2; usedIds.has(id); n++)
322
+ id = `${itemId([entry])}~${n}`;
323
+ onAddItem({ id, words: [toResultWord(entry)] });
324
324
  }
325
325
  }
326
326
  setActiveCell(null);
327
327
  },
328
328
  onClose: handleCloseWordSearch,
329
- }), !hideControls && (_jsx(TableFilterPanel, { title: filterTitle, collapsible: filterCollapsible, defaultCollapsed: filterDefaultCollapsed, collapsed: filterCollapsed, onCollapsedChange: onFilterCollapsedChange, headerSummary: filterHeaderSummary, hasImage: hasImage, hasColloquial: hasColloquial, contentTypes: contentTypes, orientation: orientation, showOutline: showOutline, cutLine: cutLine, showDivLine: showDivLine, setters: { setContentTypes, setOrientation, setShowOutline, setCutLine, setShowDivLine, setShowTopInfo, setShowBottomInfo }, showCutLine: (pagesEnabled ? itemsPerPage : count) > 1, showDivLine2: hasPairs, middleSections: middleSections, extraSelected: middleSelected, onExtraSelect: handleMiddleSelect, showPaperBands: 띠쓰나, showTopInfo: showTopInfo, showBottomInfo: showBottomInfo })), isPaginated && pages ? (_jsx("div", { className: "word-table-block__pages", children: pages.map((page, i) => (_jsx(WordTable, { orientation: orientation, contentTypes: contentTypes, cutLine: cutLine, showOutline: showOutline, showDivLine: showDivLine, count: itemsPerPage, items: page, onRemoveItem: onRemoveItem,
329
+ }), !hideControls && (_jsx(TableFilterPanel, { title: filterTitle, collapsible: filterCollapsible, defaultCollapsed: filterDefaultCollapsed, collapsed: filterCollapsed, onCollapsedChange: onFilterCollapsedChange, headerSummary: filterHeaderSummary, hasImage: hasImage, hasColloquial: hasColloquial, contentTypes: contentTypes, orientation: orientation, showOutline: showOutline, cutLine: cutLine, showDivLine: showDivLine, setters: { setContentTypes, setOrientation, setShowOutline, setCutLine, setShowDivLine, setShowTopInfo, setShowBottomInfo }, showCutLine: (pagesEnabled ? itemsPerPage : count) > 1, showDivLine2: hasPairs, middleSections: middleSections, extraSelected: middleSelected, onExtraSelect: handleMiddleSelect, showPaperBands: showBand, showTopInfo: showTopInfo, showBottomInfo: showBottomInfo })), isPaginated && pages ? (_jsx("div", { className: "word-table-block__pages", children: pages.map((page, i) => (_jsx(WordTable, { orientation: orientation, contentTypes: contentTypes, cutLine: cutLine, showOutline: showOutline, showDivLine: showDivLine, count: itemsPerPage, items: page, onRemoveItem: onRemoveItem,
330
330
  /* 쪽이 나뉘면 끌어 옮기기가 통째로 꺼졌다(말귀 단어 찾기: 9개부터). 쪽 안의 자리를 전체 자리로 옮겨 넘긴다 —
331
331
  쪽을 넘는 이동은 하지 않는다. */
332
332
  onReorder: onReorder && ((from, to) => onReorder(from + i * itemsPerPage, to + i * itemsPerPage)),
333
333
  /* 안쪽 표는 제 쪽 안의 번호를 준다 — 부르는 쪽에는 **표 전체 기준**으로 넘긴다
334
334
  (위 `onReorder` 와 같은 셈이다. 쪽이 몇으로 나뉘었는지는 부르는 쪽 일이 아니다). */
335
- renderCellAction: renderCellAction && ((n, item) => renderCellAction(n + i * itemsPerPage, item)), brand: 띠, code: code, contentRef: el => { pageRefs.current[i] = el; }, pageNumber: { current: i + 1, total: pages.length }, pageNumberPosition: pageNumberPosition }, i))) })) : (_jsx(WordTable, { orientation: orientation, contentTypes: contentTypes, cutLine: cutLine, showOutline: showOutline, showDivLine: showDivLine, count: count, items: isEditable ? undefined : (items ?? []), editCells: isEditable ? visibleCells : undefined, activeCell: activeCell, onActivateCell: (isEditable || onAddItem) ? setActiveCell : undefined, onRemoveCell: isEditable ? handleRemove : undefined, onRemoveItem: !isEditable ? onRemoveItem : undefined, onReorder: !isEditable ? onReorder : undefined,
335
+ renderCellAction: renderCellAction && ((n, item) => renderCellAction(n + i * itemsPerPage, item)), brand: band, code: code, contentRef: el => { pageRefs.current[i] = el; }, pageNumber: { current: i + 1, total: pages.length }, pageNumberPosition: pageNumberPosition }, i))) })) : (_jsx(WordTable, { orientation: orientation, contentTypes: contentTypes, cutLine: cutLine, showOutline: showOutline, showDivLine: showDivLine, count: count, items: isEditable ? undefined : (items ?? []), editCells: isEditable ? visibleCells : undefined, activeCell: activeCell, onActivateCell: (isEditable || onAddItem) ? setActiveCell : undefined, onRemoveCell: isEditable ? handleRemove : undefined, onRemoveItem: !isEditable ? onRemoveItem : undefined, onReorder: !isEditable ? onReorder : undefined,
336
336
  /* 편집 모드에는 얹을 항목이 없다 — 칸을 손으로 채우는 꼴이다 */
337
- renderCellAction: !isEditable ? renderCellAction : undefined, brand: 띠, code: code, showFillerAdd: !!onAddItem, contentRef: contentRef })), !hideControls && !saveOnly && (onReset || actionsLeftExtra != null) && (_jsxs("div", { className: "word-table-block__table-actions", children: [onReset && (_jsx(Button, { htmlType: "button", size: 48, type: "bar", label: resolvedResetLabel, onClick: onReset })), actionsLeftExtra] })), inlineNameFields && !hideControls && (_jsxs(Panel, { size: "medium", labelWidth: 64, useBorder: false, useRowBorder: false, className: "word-table-block__meta", children: [_jsx(PanelRow, { variant: "custom", label: t('wordTable.save.nameLabel', '이름'), required: true, align: "vertical", useFullWidth: true, children: _jsx(TextField, { required: true, value: nameValue ?? '', onChange: onNameChange, placeholder: namePlaceholder ? namePlaceholder + t('wordTable.save.autoFillHint', ' (Tab 또는 space 입력시 자동 입력)') : t('wordTable.save.placeholder', '이 단어표에 대한 제목을 입력하세요.'),
337
+ renderCellAction: !isEditable ? renderCellAction : undefined, brand: band, code: code, showFillerAdd: !!onAddItem, contentRef: contentRef })), !hideControls && !saveOnly && (onReset || actionsLeftExtra != null) && (_jsxs("div", { className: "word-table-block__table-actions", children: [onReset && (_jsx(Button, { htmlType: "button", size: 48, type: "bar", label: resolvedResetLabel, onClick: onReset })), actionsLeftExtra] })), inlineNameFields && !hideControls && (_jsxs(Panel, { size: "medium", labelWidth: 64, useBorder: false, useRowBorder: false, className: "word-table-block__meta", children: [_jsx(PanelRow, { variant: "custom", label: t('wordTable.save.nameLabel', '이름'), required: true, align: "vertical", useFullWidth: true, children: _jsx(TextField, { required: true, value: nameValue ?? '', onChange: onNameChange, placeholder: namePlaceholder ? namePlaceholder + t('wordTable.save.autoFillHint', ' (Tab 또는 space 입력시 자동 입력)') : t('wordTable.save.placeholder', '이 단어표에 대한 제목을 입력하세요.'),
338
338
  /* PanelRow의 라벨은 시각적으로만 붙어 있고 입력과 연결돼 있지 않아, 낭독기에는
339
339
  이름 없는 입력으로 읽혔다. placeholder는 자동완성 안내문("Tab 또는 space…")이
340
340
  섞여 있어 이름으로 부적절하므로 같은 라벨 문구를 넘긴다. */
@@ -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
- import { 한글순 } from '../../utils/textSort';
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,15 +38,15 @@ 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
- out.sort((a, b) => a.rate - b.rate || b.tried - a.tried || 한글순(a.label, b.label) || 한글순(a.key ?? a.label, b.key ?? b.label));
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;
51
51
  }
52
52
  /**
@@ -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>에서 온다.
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * 화면도 통신도 없다. 표 목록과 약한 낱말만 받아 순서를 매긴다.
9
9
  */
10
- import { 한글순 } from '../../utils/textSort';
10
+ import { compareKorean } from '../../utils/textSort';
11
11
  /**
12
12
  * 약한 낱말을 많이 품은 표부터 내놓는다.
13
13
  *
@@ -23,18 +23,18 @@ import { 한글순 } 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
  }
@@ -44,7 +44,7 @@ export function recommendTables(tables, weakLabels, limit = 5) {
44
44
  }
45
45
  out.sort((a, b) => b.covered.length - a.covered.length
46
46
  || a.totalCells - b.totalCells
47
- || 한글순(a.name, b.name));
47
+ || compareKorean(a.name, b.name));
48
48
  return out.slice(0, Math.max(0, limit));
49
49
  }
50
50
  /**
@@ -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
  }