@ihabdevteam/core 0.322.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 (196) hide show
  1. package/CHANGELOG.md +517 -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 +54 -16
  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 +13 -2
  149. package/dist/packages/core/src/i18n/locales/ko.json +12 -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 +118 -102
  174. package/dist/packages/core/src/utils/phonemeSectionCodes.d.ts +52 -0
  175. package/dist/packages/core/src/utils/phonemeSectionCodes.js +76 -0
  176. package/dist/packages/core/src/utils/prefetchChunk.js +6 -6
  177. package/dist/packages/core/src/utils/safeHref.js +4 -4
  178. package/dist/packages/core/src/utils/savedWordTables.js +5 -5
  179. package/dist/packages/core/src/utils/speak.js +12 -12
  180. package/dist/packages/core/src/utils/textSort.d.ts +0 -10
  181. package/dist/packages/core/src/utils/textSort.js +38 -48
  182. package/dist/packages/core/src/utils/theme.js +3 -3
  183. package/dist/packages/core/src/utils/trainingItemResults.js +3 -3
  184. package/dist/packages/core/src/utils/ttsConfig.js +2 -2
  185. package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +45 -45
  186. package/dist/packages/core/src/utils/wordContrast.js +3 -3
  187. package/dist/packages/core/src/utils/wordPairIndex.js +4 -4
  188. package/dist/packages/core/src/utils/wordTrainingGenerators.js +4 -4
  189. package/dist/packages/core/src/utils/wordTrainingPool.js +15 -15
  190. package/dist/packages/core/src/utils/wordTrainingRuntime.js +6 -6
  191. package/dist/packages/core/src/utils/wornDate.d.ts +3 -3
  192. package/dist/packages/core/src/utils/wornDate.js +14 -14
  193. package/dist/packages/core/src/version.d.ts +1 -1
  194. package/dist/packages/core/src/version.js +1 -1
  195. package/dist/utils.cjs.js +4 -4
  196. package/package.json +1 -1
@@ -41,13 +41,13 @@ export function todayWeekValue() {
41
41
  export function buildMonthChartData(points) {
42
42
  const now = getTodayKstDate();
43
43
  const curKey = `${now.getFullYear()}년 ${now.getMonth() + 1}월`;
44
- const 달키 = (y, m0) => `${y}년 ${m0 + 1}월`;
44
+ const monthKey = (y, m0) => `${y}년 ${m0 + 1}월`;
45
45
  const byMonth = new Map();
46
46
  byMonth.set(curKey, []);
47
47
  for (const p of points) {
48
48
  if (isNaN(p.date.getTime()))
49
49
  continue;
50
- const key = 달키(p.date.getFullYear(), p.date.getMonth());
50
+ const key = monthKey(p.date.getFullYear(), p.date.getMonth());
51
51
  if (!byMonth.has(key))
52
52
  byMonth.set(key, []);
53
53
  byMonth.get(key).push(p);
@@ -61,16 +61,16 @@ export function buildMonthChartData(points) {
61
61
  continue;
62
62
  const year = Number(m[1]);
63
63
  const month0 = Number(m[2]) - 1;
64
- const 첫월 = firstMondayOfMonth(year, month0);
65
- const 오늘이앞 = key === curKey && now.getDate() < 첫월;
66
- if (!오늘이앞 && !pts.some(p => p.date.getDate() < 첫월))
64
+ const firstMonday = firstMondayOfMonth(year, month0);
65
+ const todayBeforeFirst = key === curKey && now.getDate() < firstMonday;
66
+ if (!todayBeforeFirst && !pts.some(p => p.date.getDate() < firstMonday))
67
67
  continue;
68
68
  const prev = new Date(year, month0, 0);
69
- const prevKey = 달키(prev.getFullYear(), prev.getMonth());
69
+ const prevKey = monthKey(prev.getFullYear(), prev.getMonth());
70
70
  if (!byMonth.has(prevKey))
71
71
  byMonth.set(prevKey, []);
72
72
  }
73
- const 칸들 = new Map();
73
+ const cells = new Map();
74
74
  for (const [key, pts] of byMonth) {
75
75
  const m = key.match(/^(\d+)년 (\d+)월$/);
76
76
  if (!m)
@@ -96,18 +96,18 @@ export function buildMonthChartData(points) {
96
96
  for (let k = 0; k < partsLen; k++)
97
97
  dayPartsB[d][k] += p.partsB[k] ?? 0;
98
98
  }
99
- 칸들.set(key, { year, month, daysInMonth, partsLen, hasPartsB, dayA, dayB, dayParts, dayPartsB });
99
+ cells.set(key, { year, month, daysInMonth, partsLen, hasPartsB, dayA, dayB, dayParts, dayPartsB });
100
100
  }
101
101
  /** 그 날이 오늘 뒤인가 — 다음 달 날을 이어 붙일 때 미래는 세지 않는다 */
102
- const 미래 = (y, m0, d) => new Date(y, m0, d).getTime() > new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
102
+ const isFuture = (y, m0, d) => new Date(y, m0, d).getTime() > new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
103
103
  const result = {};
104
- for (const [key, 칸] of 칸들) {
105
- const { year, month, daysInMonth, dayA, dayB, dayParts, dayPartsB } = 칸;
104
+ for (const [key, cell] of cells) {
105
+ const { year, month, daysInMonth, dayA, dayB, dayParts, dayPartsB } = cell;
106
106
  const isCurMonth = year === now.getFullYear() && month === now.getMonth();
107
107
  const today = now.getDate();
108
- const 다음 = 칸들.get(달키(month === 11 ? year + 1 : year, (month + 1) % 12));
108
+ const nextCell = cells.get(monthKey(month === 11 ? year + 1 : year, (month + 1) % 12));
109
109
  /* 마지막 주가 다음 달 계열을 더 많이 가질 수 있다 — 그 주만 넓힌다 */
110
- const partsLen = 칸.partsLen;
110
+ const partsLen = cell.partsLen;
111
111
  const daily = [];
112
112
  for (let d = 1; d <= daysInMonth; d++) {
113
113
  const isFuture = isCurMonth && d > today;
@@ -118,16 +118,16 @@ export function buildMonthChartData(points) {
118
118
  a: isFuture && dayA[d] === 0 && dayB[d] === 0 ? -1 : dayA[d],
119
119
  b: dayB[d],
120
120
  ...(partsLen ? { parts: dayParts[d] } : {}),
121
- ...(partsLen && 칸.hasPartsB ? { partsB: dayPartsB[d] } : {}),
121
+ ...(partsLen && cell.hasPartsB ? { partsB: dayPartsB[d] } : {}),
122
122
  });
123
123
  }
124
124
  // 월요일 시작 — 그 달의 '첫 월요일'부터 7일 단위로 버킷. 첫 월요일 이전 날짜는
125
125
  // 이전 달의 마지막(걸침) 주에 속하므로 이 달에서는 제외한다. 마지막 주는 다음 달로 넘어간다.
126
126
  const weekly = [];
127
127
  for (let start = firstMondayOfMonth(year, month); start <= daysInMonth; start += 7) {
128
- const 넘침 = Math.max(0, start + 6 - daysInMonth); // 다음 달에서 이어 붙일 날 수
129
- const wLen = 넘침 && 다음 ? Math.max(partsLen, 다음.partsLen) : partsLen;
130
- const wHasB = 칸.hasPartsB || !!(넘침 && 다음?.hasPartsB);
128
+ const spill = Math.max(0, start + 6 - daysInMonth); // 다음 달에서 이어 붙일 날 수
129
+ const wLen = spill && nextCell ? Math.max(partsLen, nextCell.partsLen) : partsLen;
130
+ const wHasB = cell.hasPartsB || !!(spill && nextCell?.hasPartsB);
131
131
  let a = 0;
132
132
  let b = 0;
133
133
  const wParts = new Array(wLen).fill(0);
@@ -140,20 +140,20 @@ export function buildMonthChartData(points) {
140
140
  wPartsB[k] += dayPartsB[d][k];
141
141
  }
142
142
  }
143
- if (넘침 && 다음) {
144
- for (let d = 1; d <= 넘침; d++) {
145
- if (미래(다음.year, 다음.month, d))
143
+ if (spill && nextCell) {
144
+ for (let d = 1; d <= spill; d++) {
145
+ if (isFuture(nextCell.year, nextCell.month, d))
146
146
  break;
147
- a += 다음.dayA[d];
148
- b += 다음.dayB[d];
149
- for (let k = 0; k < 다음.partsLen; k++) {
150
- wParts[k] += 다음.dayParts[d][k];
151
- wPartsB[k] += 다음.dayPartsB[d][k];
147
+ a += nextCell.dayA[d];
148
+ b += nextCell.dayB[d];
149
+ for (let k = 0; k < nextCell.partsLen; k++) {
150
+ wParts[k] += nextCell.dayParts[d][k];
151
+ wPartsB[k] += nextCell.dayPartsB[d][k];
152
152
  }
153
153
  }
154
154
  }
155
- const 끝 = 넘침 ? `${(month + 1) % 12 + 1}/${넘침}` : `${start + 6}`;
156
- weekly.push({ label: `${weekly.length + 1}주`, sublabel: `[${start}일~${끝}일]`, a, b, ...(wLen ? { parts: wParts } : {}), ...(wLen && wHasB ? { partsB: wPartsB } : {}) });
155
+ const endLabel = spill ? `${(month + 1) % 12 + 1}/${spill}` : `${start + 6}`;
156
+ weekly.push({ label: `${weekly.length + 1}주`, sublabel: `[${start}일~${endLabel}일]`, a, b, ...(wLen ? { parts: wParts } : {}), ...(wLen && wHasB ? { partsB: wPartsB } : {}) });
157
157
  }
158
158
  result[key] = { weekly, daily };
159
159
  }
@@ -178,5 +178,25 @@ export const MONTH_OPTIONS = genMonthOptions();
178
178
  /** 지금 시각 기준 월 옵션 — 모듈 상수(MONTH_OPTIONS)는 불러온 순간의 달에 굳어, 탭을 열어 둔 채 달이 넘어가면 이번 달이
179
179
  * 옵션에서 빠졌다. 컨트롤 훅은 자료가 바뀔 때마다 이것으로 다시 만든다. */
180
180
  export function monthOptionsNow() { return genMonthOptions(); }
181
- // 한글 서수(주차) — '첫째주', '둘째주' …
182
- export const KO_ORD = ['첫째', '둘째', '셋째', '넷째', '다섯째', '여섯째', '일곱째'];
181
+ /**
182
+ * 몇째 주인가를 **지금 언어로** 적는다 — `0` 이 첫째 주다.
183
+ *
184
+ * **언어 가름을 부르는 쪽이 하지 않는다.** 전에는 한글 서수 배열(`KO_ORD`)을 내주고
185
+ * 쓰는 쪽마다 `lang.startsWith('ko') ? KO_ORD[w] + '주' : 'Week ' + (w+1)` 를 되풀이했다 —
186
+ * 코어 두 자리와 말귀 한 자리가 **같은 가름을 각자** 적고 있었다. 말이 하나 늘면 세 자리를
187
+ * 다 고쳐야 하고, 한 자리만 안 고치면 화면에서 갈린다.
188
+ *
189
+ * 글귀는 사전이 쥔다(`calendar.weekLabel`·`calendar.weekOrdinal`) — 한국어는 서수가
190
+ * 규칙으로 안 나와(첫째·둘째…) 표가 필요하고, **그 표의 자리는 코드가 아니라 사전이다.**
191
+ * 서수 표가 없는 말은 `{{n}}` 으로 적는다(영어 `Week 3`).
192
+ *
193
+ * @public
194
+ */
195
+ export function weekLabel(t, week) {
196
+ const ords = t('calendar.weekOrdinal', { returnObjects: true, defaultValue: [] });
197
+ const ord = Array.isArray(ords) && typeof ords[week] === 'string' ? ords[week] : null;
198
+ /* **변수는 하나다(`ord`).** 말마다 다른 변수를 쓰면(`{{ord}}` vs `{{n}}`) 사전 지킴이가
199
+ 운다 — 로케일을 바꿀 때 `{{변수}}` 가 그대로 보이거나 값이 비는 꼴이다. 영어 서수 표는
200
+ 숫자 그대로(`'1'`…)여서 `Week {{ord}}` 가 같은 변수로 맞는다. */
201
+ return t('calendar.weekLabel', { ord: ord ?? String(week + 1) });
202
+ }
@@ -4,12 +4,12 @@ const ymdOf = (y, m0, d) => {
4
4
  const t = new Date(Date.UTC(y, m0, d));
5
5
  return `${t.getUTCFullYear()}-${String(t.getUTCMonth() + 1).padStart(2, '0')}-${String(t.getUTCDate()).padStart(2, '0')}`;
6
6
  };
7
- const 범위 = (y, m0, 첫날, 날수) => ({
8
- from: kstStartOfDay(y, m0, 첫날),
7
+ const rangeOf = (y, m0, firstDay, dayCount) => ({
8
+ from: kstStartOfDay(y, m0, firstDay),
9
9
  // 끝은 그날의 **마지막 밀리초까지** — 23:59:59.000 이면 마지막 999ms 에 남은 기록이 목록에서 빠졌다
10
- to: new Date(kstStartOfDay(y, m0, 첫날 + 날수).getTime() - 1),
11
- fromYmd: ymdOf(y, m0, 첫날),
12
- toYmd: ymdOf(y, m0, 첫날 + 날수 - 1),
10
+ to: new Date(kstStartOfDay(y, m0, firstDay + dayCount).getTime() - 1),
11
+ fromYmd: ymdOf(y, m0, firstDay),
12
+ toYmd: ymdOf(y, m0, firstDay + dayCount - 1),
13
13
  });
14
14
  /**
15
15
  * 현재 컨트롤 선택(주/월)의 기간 — 상세보기(기간별 리스트)용. **KST 의 날로 자른다**(해외 기기도 같은 기간).
@@ -23,8 +23,8 @@ export function getControlDateRange(control, offset = 0) {
23
23
  const today = getTodayKstDate();
24
24
  const y = m ? Number(m[1]) : today.getFullYear();
25
25
  const m0 = (m ? Number(m[2]) : today.getMonth() + 1) - 1 + offset;
26
- const 날수 = new Date(Date.UTC(y, m0 + 1, 0)).getUTCDate();
27
- return 범위(y, m0, 1, 날수);
26
+ const dayCount = new Date(Date.UTC(y, m0 + 1, 0)).getUTCDate();
27
+ return rangeOf(y, m0, 1, dayCount);
28
28
  }
29
29
  const opt = control.weekRanges.find(o => o.value === control.weekRange);
30
30
  if (!opt) {
@@ -33,5 +33,5 @@ export function getControlDateRange(control, offset = 0) {
33
33
  return { from: n, to: n, fromYmd: ymd, toYmd: ymd };
34
34
  }
35
35
  const firstMon = firstMondayOfMonth(opt.year, opt.month - 1);
36
- return 범위(opt.year, opt.month - 1, firstMon + (opt.week + offset) * 7, 7);
36
+ return rangeOf(opt.year, opt.month - 1, firstMon + (opt.week + offset) * 7, 7);
37
37
  }
@@ -10,23 +10,23 @@ export function splitTrackList(template) {
10
10
  if (!template)
11
11
  return [];
12
12
  const out = [];
13
- let 깊이 = 0;
14
- let 조각 = '';
13
+ let depth = 0;
14
+ let piece = '';
15
15
  for (const c of template.trim()) {
16
16
  if (c === '(')
17
- 깊이++;
17
+ depth++;
18
18
  else if (c === ')')
19
- 깊이--;
20
- if (/\s/.test(c) && 깊이 === 0) {
21
- if (조각) {
22
- out.push(조각);
23
- 조각 = '';
19
+ depth--;
20
+ if (/\s/.test(c) && depth === 0) {
21
+ if (piece) {
22
+ out.push(piece);
23
+ piece = '';
24
24
  }
25
25
  continue;
26
26
  }
27
- 조각 += c;
27
+ piece += c;
28
28
  }
29
- if (조각)
30
- out.push(조각);
29
+ if (piece)
30
+ out.push(piece);
31
31
  return out;
32
32
  }
@@ -7,11 +7,11 @@
7
7
  *
8
8
  * 같은 fixed 기준(뷰포트, 또는 transform 을 건 조상 — 놀이터 시연 틀)을 쓰는 자리끼리만 쌓는다.
9
9
  */
10
- const 자리수 = new Map();
11
- let 관찰 = null;
10
+ const offsets = new Map();
11
+ let observer = null;
12
12
  const GAP = 12;
13
13
  /* fixed 의 기준이 되는 조상 — transform·filter·perspective·contain 이 걸린 가장 가까운 조상, 없으면 문서 */
14
- function 기준찾기(el) {
14
+ function rootOf(el) {
15
15
  for (let p = el.parentElement; p && p !== document.body; p = p.parentElement) {
16
16
  const cs = window.getComputedStyle(p);
17
17
  if ((cs.transform && cs.transform !== 'none') || (cs.filter && cs.filter !== 'none') || (cs.perspective && cs.perspective !== 'none') || /paint|layout|strict|content/.test(cs.contain || ''))
@@ -19,27 +19,27 @@ function 기준찾기(el) {
19
19
  }
20
20
  return document;
21
21
  }
22
- function 다시쌓기() {
23
- const 무리 = new Map();
24
- for (const el of 자리수.keys()) {
22
+ function restack() {
23
+ const byRoot = new Map();
24
+ for (const el of offsets.keys()) {
25
25
  if (!el.isConnected)
26
26
  continue;
27
- const k = 기준찾기(el);
28
- const 줄 = 무리.get(k) ?? [];
29
- 줄.push(el);
30
- 무리.set(k, 줄);
27
+ const k = rootOf(el);
28
+ const list = byRoot.get(k) ?? [];
29
+ list.push(el);
30
+ byRoot.set(k, list);
31
31
  }
32
- for (const 줄 of 무리.values()) {
33
- 줄.sort((a, b) => (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1));
34
- let 아래로 = 0;
35
- for (const el of 줄) {
36
- if (아래로 > 0)
37
- el.style.setProperty('--update-alert-stack-offset', `${아래로}px`);
32
+ for (const list of byRoot.values()) {
33
+ list.sort((a, b) => (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1));
34
+ let offset = 0;
35
+ for (const el of list) {
36
+ if (offset > 0)
37
+ el.style.setProperty('--update-alert-stack-offset', `${offset}px`);
38
38
  else
39
39
  el.style.removeProperty('--update-alert-stack-offset');
40
40
  const h = el.getBoundingClientRect().height;
41
41
  if (h > 0)
42
- 아래로 += h + GAP;
42
+ offset += h + GAP;
43
43
  }
44
44
  }
45
45
  }
@@ -47,22 +47,22 @@ function 다시쌓기() {
47
47
  export function registerUpdateAlertSlot(el) {
48
48
  if (typeof window === 'undefined')
49
49
  return () => { };
50
- 자리수.set(el, (자리수.get(el) ?? 0) + 1);
51
- if (자리수.get(el) === 1 && typeof ResizeObserver !== 'undefined') {
52
- 관찰 ??= new ResizeObserver(() => 다시쌓기());
53
- 관찰.observe(el);
50
+ offsets.set(el, (offsets.get(el) ?? 0) + 1);
51
+ if (offsets.get(el) === 1 && typeof ResizeObserver !== 'undefined') {
52
+ observer ??= new ResizeObserver(() => restack());
53
+ observer.observe(el);
54
54
  }
55
- 다시쌓기();
55
+ restack();
56
56
  return () => {
57
- const n = (자리수.get(el) ?? 1) - 1;
57
+ const n = (offsets.get(el) ?? 1) - 1;
58
58
  if (n > 0) {
59
- 자리수.set(el, n);
60
- 다시쌓기();
59
+ offsets.set(el, n);
60
+ restack();
61
61
  return;
62
62
  }
63
- 자리수.delete(el);
64
- 관찰?.unobserve(el);
63
+ offsets.delete(el);
64
+ observer?.unobserve(el);
65
65
  el.style.removeProperty('--update-alert-stack-offset');
66
- 다시쌓기();
66
+ restack();
67
67
  };
68
68
  }
@@ -1,14 +1,14 @@
1
1
  'use client';
2
2
  import { useState, useEffect, useMemo, useCallback } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
- import { firstMondayOfMonth, todayWeekValue, monthOptionsNow, KO_ORD } from './barChartData';
4
+ import { firstMondayOfMonth, todayWeekValue, monthOptionsNow, weekLabel } from './barChartData';
5
5
  /**
6
6
  * BarChart 기간 상태/옵션/네비게이션을 캡슐화한 훅.
7
7
  * 여러 BarChart가 하나의 컨트롤을 공유하려면 이 훅을 상위에서 호출해 control을 내려준다.
8
8
  * @public
9
9
  */
10
10
  export function useBarChartControl(data) {
11
- const { t, i18n } = useTranslation();
11
+ const { t } = useTranslation();
12
12
  const [period, setPeriod] = useState('week');
13
13
  // 달 옵션·오늘은 자료가 바뀔 때 다시 센다 — 모듈 로드 때 한 번 세면 자정을 넘긴 탭에서 이번 달·오늘 주가 옛날에 굳었다
14
14
  const DATA_MONTH_OPTIONS = useMemo(() => monthOptionsNow().filter(o => o.value in data), [data]);
@@ -26,10 +26,8 @@ export function useBarChartControl(data) {
26
26
  }, [DATA_MONTH_OPTIONS]);
27
27
  const weekOptionLabel = useCallback((o) => {
28
28
  const m = t('calendar.yearMonth', { year: o.year, month: o.month });
29
- return i18n.language?.startsWith('ko')
30
- ? `${m} ${KO_ORD[o.week] ?? `${o.week + 1}`}주`
31
- : `${m} Week ${o.week + 1}`;
32
- }, [t, i18n.language]);
29
+ return `${m} ${weekLabel(t, o.week)}`;
30
+ }, [t]);
33
31
  const monthOptions = useMemo(() => DATA_MONTH_OPTIONS.map(o => ({
34
32
  value: o.value, label: t('calendar.yearMonth', { year: o.year, month: o.month }),
35
33
  })), [t, DATA_MONTH_OPTIONS]);
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
3
3
  /** 글자 하나든 한 줄이든 **줄에 적히는 말**을 꺼낸다 */
4
- function 라벨(x) {
4
+ function labelOf(x) {
5
5
  return typeof x === 'string' ? x : x.label;
6
6
  }
7
7
  /**
@@ -18,7 +18,7 @@ export function useComboboxSuggestions({ value, onChange, suggestions, openOnFoc
18
18
  const [open, setOpen] = useState(false);
19
19
  const [activeIdx, setActiveIdx] = useState(-1);
20
20
  const blurTimerRef = useRef(null);
21
- const 계산한것 = useMemo(() => {
21
+ const computed = useMemo(() => {
22
22
  if (!suggestions.length)
23
23
  return [];
24
24
  const v = value ?? '';
@@ -30,40 +30,40 @@ export function useComboboxSuggestions({ value, onChange, suggestions, openOnFoc
30
30
  return openOnFocus ? suggestions : [];
31
31
  if (typeof filterSuggestions === 'function')
32
32
  return filterSuggestions(suggestions, v);
33
- return suggestions.filter(s => 라벨(s).toLowerCase().includes(v.toLowerCase()));
33
+ return suggestions.filter(s => labelOf(s).toLowerCase().includes(v.toLowerCase()));
34
34
  }, [suggestions, value, openOnFocus, filterSuggestions]);
35
35
  /* **한글 조합 중에는 0 건이 되어도 직전 목록을 둔다.** '사'→'삭'→'사고' 로 치는 동안 중간값 '삭' 에 걸리는 후보가 없어
36
36
  목록이 한 박자 사라졌다 돌아왔다(깜빡임·스크린리더가 목록 닫힘을 알림). 조합이 끝나면(compositionend) 다시 계산한다.
37
37
  부르는 쪽이 suggestions 를 값으로 미리 거른 경우(matchesWordQuery 순위 등)도 같다 — 순위 규칙은 건드리지 않는다. */
38
- const [조합중, set조합중] = useState(false);
39
- const 직전목록 = useRef([]);
40
- const filtered = 조합중 && 계산한것.length === 0 ? 직전목록.current : 계산한것;
38
+ const [composing, setComposing] = useState(false);
39
+ const prevList = useRef([]);
40
+ const filtered = composing && computed.length === 0 ? prevList.current : computed;
41
41
  useEffect(() => { if (filtered.length)
42
- 직전목록.current = filtered; }, [filtered]);
43
- const handleCompositionStart = useCallback(() => set조합중(true), []);
44
- const handleCompositionEnd = useCallback(() => set조합중(false), []);
42
+ prevList.current = filtered; }, [filtered]);
43
+ const handleCompositionStart = useCallback(() => setComposing(true), []);
44
+ const handleCompositionEnd = useCallback(() => setComposing(false), []);
45
45
  // 메뉴가 닫히면 활성 인덱스를 리셋하는 의도된 동기화다
46
46
  // eslint-disable-next-line react-hooks/set-state-in-effect
47
- const 처음짚는자리 = autoActiveFirst ? 0 : -1;
47
+ const initialActiveIdx = autoActiveFirst ? 0 : -1;
48
48
  useEffect(() => { if (!open)
49
- setActiveIdx(처음짚는자리); }, [open, 처음짚는자리]);
49
+ setActiveIdx(initialActiveIdx); }, [open, initialActiveIdx]);
50
50
  useEffect(() => () => { if (blurTimerRef.current)
51
51
  clearTimeout(blurTimerRef.current); }, []);
52
52
  /* 입력으로 후보가 줄면 가리키던 번호가 목록 밖에 남는다 — 그 채로 Enter 를 누르면 filtered[3] 같은
53
53
  없는 칸을 골라 onChange(undefined) 가 나갔다. 밖이면 '안 가리킴' 으로 본다. */
54
- const safeIdx = activeIdx < filtered.length ? activeIdx : 처음짚는자리 < filtered.length ? 처음짚는자리 : -1;
54
+ const safeIdx = activeIdx < filtered.length ? activeIdx : initialActiveIdx < filtered.length ? initialActiveIdx : -1;
55
55
  const handleSelect = useCallback((item) => {
56
56
  /* 고르는 것이 '값 채우기' 가 아닌 자리가 있다 — 그때는 칸을 안 건드린다 */
57
57
  if (onSelectSuggestion)
58
58
  onSelectSuggestion(item);
59
59
  else
60
- onChange?.(라벨(item));
60
+ onChange?.(labelOf(item));
61
61
  setOpen(false);
62
62
  }, [onChange, onSelectSuggestion]);
63
63
  const handleFocus = useCallback(() => setOpen(true), []);
64
64
  /* 글자를 치면 목록을 다시 연다 — 고르거나 Esc 로 닫은 뒤에는 포커스를 뺐다 넣어야만 열렸다.
65
65
  새 글자에는 새 후보가 서므로 가리킴도 처음으로 돌린다. */
66
- const handleInput = useCallback(() => { setOpen(true); setActiveIdx(처음짚는자리); }, [처음짚는자리]);
66
+ const handleInput = useCallback(() => { setOpen(true); setActiveIdx(initialActiveIdx); }, [initialActiveIdx]);
67
67
  // blur가 리스트 항목의 onMouseDown보다 먼저 발생하는 걸 막기 위한 딜레이
68
68
  const handleBlur = useCallback(() => {
69
69
  blurTimerRef.current = setTimeout(() => setOpen(false), 120);
@@ -76,12 +76,12 @@ export function useComboboxSuggestions({ value, onChange, suggestions, openOnFoc
76
76
  if (e.key === 'ArrowDown') {
77
77
  e.preventDefault();
78
78
  setOpen(true);
79
- setActiveIdx(i => Math.min(filtered.length - 1, (i < filtered.length ? i : 처음짚는자리) + 1));
79
+ setActiveIdx(i => Math.min(filtered.length - 1, (i < filtered.length ? i : initialActiveIdx) + 1));
80
80
  }
81
81
  else if (e.key === 'ArrowUp') {
82
82
  e.preventDefault();
83
83
  /* 첫 줄을 늘 짚는 칸에서는 **안 짚은 상태로 못 올라간다** — 그 상태가 없다 */
84
- setActiveIdx(i => Math.max(처음짚는자리, (i < filtered.length ? i : filtered.length) - 1));
84
+ setActiveIdx(i => Math.max(initialActiveIdx, (i < filtered.length ? i : filtered.length) - 1));
85
85
  }
86
86
  else if (e.key === 'Enter') {
87
87
  // 한글 조합을 끝내는 Enter는 선택이 아니다 — 조합 중엔 제안을 고르지 않는다
@@ -94,7 +94,7 @@ export function useComboboxSuggestions({ value, onChange, suggestions, openOnFoc
94
94
  setOpen(false);
95
95
  } // 제안이 열렸을 때만 소비(상위 팝업 ESC보다 우선하지 않도록)
96
96
  }
97
- }, [filtered, safeIdx, open, handleSelect, 처음짚는자리]);
97
+ }, [filtered, safeIdx, open, handleSelect, initialActiveIdx]);
98
98
  return {
99
99
  listId, open, activeIdx: safeIdx, filtered, handleFocus, handleBlur, handleInput, handleKeyDown, handleSelect,
100
100
  /** 입력칸의 onCompositionStart 에 건다 — 조합 중에는 걸리는 후보가 0 건이 되어도 직전 목록을 둔다 */
@@ -21,16 +21,16 @@ export function useToastLifecycle({ message, position, duration, onClose }) {
21
21
  const DURATION_MS = duration;
22
22
  const hideTimerRef = useRef(null);
23
23
  /* 멈췄다 다시 돌릴 때 **남은 만큼만** 돌리려고 언제 시작했는지와 남은 값을 들고 있는다 */
24
- const 시작한때Ref = useRef(0);
25
- const 남은Ref = useRef(0);
26
- const 멈춤Ref = useRef(false);
24
+ const startedAtRef = useRef(0);
25
+ const remainingRef = useRef(0);
26
+ const pausedRef = useRef(false);
27
27
  const exitTimerRef = useRef(null);
28
28
  const rafOuterRef = useRef(null);
29
29
  const rafInnerRef = useRef(null);
30
30
  const styleResetTimerRef = useRef(null);
31
31
  /* 사라짐 타이머를 거는 한 곳 — 처음 걸 때도, 멈췄다 다시 돌릴 때도 여기를 쓴다 */
32
- const 사라짐걸기 = useRef(() => { });
33
- 사라짐걸기.current = (ms) => {
32
+ const scheduleDismissRef = useRef(() => { });
33
+ scheduleDismissRef.current = (ms) => {
34
34
  /* 0 은 막지 않는다 — 0 이면 "지금 사라져라" 이지 "사라지지 마라" 가 아니다.
35
35
  다만 **지금 이 값이 0 이 되는 길은 없다.** 걸 때는 부르는 쪽이
36
36
  `DURATION_MS > 0` 로 가리고, `resume()` 이 남은 0 으로 부르려면 그 전에
@@ -39,8 +39,8 @@ export function useToastLifecycle({ message, position, duration, onClose }) {
39
39
  뜻이 맞는 쪽으로만 적어 둔다. */
40
40
  if (ms < 0)
41
41
  return;
42
- 시작한때Ref.current = Date.now();
43
- 남은Ref.current = ms;
42
+ startedAtRef.current = Date.now();
43
+ remainingRef.current = ms;
44
44
  hideTimerRef.current = setTimeout(() => {
45
45
  hideTimerRef.current = null;
46
46
  setShowing(false);
@@ -54,18 +54,18 @@ export function useToastLifecycle({ message, position, duration, onClose }) {
54
54
  }, ms);
55
55
  };
56
56
  const pause = useCallback(() => {
57
- if (멈춤Ref.current || !hideTimerRef.current)
57
+ if (pausedRef.current || !hideTimerRef.current)
58
58
  return;
59
- 멈춤Ref.current = true;
59
+ pausedRef.current = true;
60
60
  clearTimeout(hideTimerRef.current);
61
61
  hideTimerRef.current = null;
62
- 남은Ref.current = Math.max(0, 남은Ref.current - (Date.now() - 시작한때Ref.current));
62
+ remainingRef.current = Math.max(0, remainingRef.current - (Date.now() - startedAtRef.current));
63
63
  }, []);
64
64
  const resume = useCallback(() => {
65
- if (!멈춤Ref.current)
65
+ if (!pausedRef.current)
66
66
  return;
67
- 멈춤Ref.current = false;
68
- 사라짐걸기.current(남은Ref.current);
67
+ pausedRef.current = false;
68
+ scheduleDismissRef.current(remainingRef.current);
69
69
  }, []);
70
70
  // message 변경 시 진입/퇴장 애니메이션과 타이머를 재구동하는 의도된 동기화다
71
71
  /* eslint-disable react-hooks/set-state-in-effect */
@@ -88,10 +88,10 @@ export function useToastLifecycle({ message, position, duration, onClose }) {
88
88
  /* 멈춘 채(마우스·포커스가 올라가 있는 채) 문구가 바뀌면 시계를 새로 걸지 않는다 — 전에는 멈춤을 풀고 걸어
89
89
  읽고 있는 알림이 손 밑에서 사라졌고, 떠날 때의 resume 은 멈춤이 이미 풀려 아무것도 안 했다.
90
90
  새 문구의 시간은 **처음부터** 남겨 두고 resume 이 그만큼 돌린다. */
91
- if (멈춤Ref.current)
92
- 남은Ref.current = DURATION_MS;
91
+ if (pausedRef.current)
92
+ remainingRef.current = DURATION_MS;
93
93
  else
94
- 사라짐걸기.current(DURATION_MS);
94
+ scheduleDismissRef.current(DURATION_MS);
95
95
  }
96
96
  lastMessageRef.current = message;
97
97
  /* 여기서 건 사라짐 타이머도 떠날 때 걷는다 — 빈 정리 함수라 문구를 바꾼 뒤 언마운트하면 타이머가 살아
@@ -154,8 +154,8 @@ export function useToastLifecycle({ message, position, duration, onClose }) {
154
154
  setShowing(true);
155
155
  }
156
156
  if (DURATION_MS > 0) {
157
- 멈춤Ref.current = false;
158
- 사라짐걸기.current(DURATION_MS);
157
+ pausedRef.current = false;
158
+ scheduleDismissRef.current(DURATION_MS);
159
159
  }
160
160
  }
161
161
  else {
@@ -4,10 +4,23 @@
4
4
  */
5
5
  import type { CSSProperties } from 'react';
6
6
  export type WordTableOrientation = 'landscape' | 'portrait';
7
- /** @public */
8
- export type WordTableContentType = '단어' | '이미지' | '표준 발음' | '구어체 발음';
9
- /** @public */
10
- export type WordTableCutLine = '표시' | '점선' | '코너' | '미표시';
7
+ /**
8
+ * 단어표 칸에 무엇을 그리나 — **코드다, 보일 글이 아니다.**
9
+ *
10
+ * 2026-10-13 한글 값(`'단어'`·`'이미지'`…)에서 옮겼다(사장님 지시 "모두 표준으로"). 저장되는
11
+ * 값이 표시 언어에 묶이면 **말을 고치려 해도 자료를 밀어야** 한다 — 보일 글은 사전이 쥐고
12
+ * (`wordTable.filter.*`), 코드는 안 바뀐다. 같은 저장소의 `wordAttributeFilterCodes` 가
13
+ * 이미 그 꼴이었다(`'grade'`·`'native'`…).
14
+ *
15
+ * 옮길 때 소비자 넷을 셌다 — `WordTableContentType`·`WordTableCutLine` 을 적는 줄이 **0** 이고,
16
+ * 저장되는 자리도 없었다(`SavedWordTable` 에 안 들고 `localStorage` 에도 안 적는다).
17
+ * 그래서 받침 없이 그냥 옮겼다 — `DeviceSide` 때와 달리 밀 자료가 없다.
18
+ *
19
+ * @public
20
+ */
21
+ export type WordTableContentType = 'word' | 'image' | 'standardPron' | 'colloquialPron';
22
+ /** 칸 사이 선 — **코드다.** 보일 글은 사전(`wordTable.filter.show`·`.dotted`·`.corner`·`.hide`). @public */
23
+ export type WordTableCutLine = 'show' | 'dashed' | 'corner' | 'hide';
11
24
  /** 페이지 분할 시 종이 안에 찍는 페이지 번호 위치 — 'none'이면 미표시 */
12
25
  /** @public */
13
26
  export type PageNumberPosition = 'left' | 'center' | 'right' | 'none';
@@ -102,10 +102,10 @@ export function gridFor(orientation, count) {
102
102
  return { cols, rows };
103
103
  }
104
104
  export const CUT_CLASS = {
105
- '표시': 'word-table-block__content--cut-show',
106
- '점선': '',
107
- '코너': 'word-table-block__content--cut-corner',
108
- '미표시': '',
105
+ show: 'word-table-block__content--cut-show',
106
+ dashed: '',
107
+ corner: 'word-table-block__content--cut-corner',
108
+ hide: '',
109
109
  };
110
110
  export const POS_LABEL = {
111
111
  '명': '명사', '동': '동사', '형': '형용사', '부': '부사',
@@ -175,7 +175,7 @@ export function expandSlashLabel(raw, labelMap) {
175
175
  return raw.split('/').map(part => labelMap[part] ?? part).join('/');
176
176
  }
177
177
  export function dotDashStyle(index, cols, rows, cutLine) {
178
- if (cutLine !== '점선')
178
+ if (cutLine !== 'dashed')
179
179
  return {};
180
180
  const col = index % cols;
181
181
  const row = Math.floor(index / cols);
@@ -3,18 +3,18 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo } from 'react';
4
4
  import i18n, { SUPPORTED_LOCALES, DEFAULT_LOCALE } from '../i18n';
5
5
  import { I18nContext } from './I18nContext';
6
- function 지원코드(locale) {
7
- const 있나 = (c) => SUPPORTED_LOCALES.some(l => l.code === c);
8
- if (있나(locale))
6
+ function supportedCode(locale) {
7
+ const has = (c) => SUPPORTED_LOCALES.some(l => l.code === c);
8
+ if (has(locale))
9
9
  return locale;
10
- const 기본 = locale.split(/[-_]/)[0]?.toLowerCase() ?? '';
11
- return 있나(기본) ? 기본 : DEFAULT_LOCALE;
10
+ const base = locale.split(/[-_]/)[0]?.toLowerCase() ?? '';
11
+ return has(base) ? base : DEFAULT_LOCALE;
12
12
  }
13
13
  /** @public */
14
14
  export function I18nProvider({ locale = DEFAULT_LOCALE, children }) {
15
15
  /* 지역이 붙은 코드('en-US'·'ko_KR')는 기본 언어로 읽는다 — 전에는 목록에 없다고 기본(ko)으로 떨어져 영어를 달라 했는데
16
16
  한국어가 나오고, context 의 locale 은 'en-US' 로 남아 둘이 갈렸다. context 에도 실제로 건 코드를 넣는다. */
17
- const code = 지원코드(locale);
17
+ const code = supportedCode(locale);
18
18
  useEffect(() => {
19
19
  if (i18n.language !== code) {
20
20
  i18n.changeLanguage(code);