@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.
- package/CHANGELOG.md +517 -0
- package/dist/components.cjs.js +183 -183
- package/dist/components.esm.css +1 -1
- package/dist/hooks.cjs.js +3 -3
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core.cjs.js +187 -187
- package/dist/ihabdevteam-core.d.ts +54 -16
- package/dist/packages/core/src/components/Accordion.js +3 -3
- package/dist/packages/core/src/components/Alert.js +2 -2
- package/dist/packages/core/src/components/BarChart.js +120 -119
- package/dist/packages/core/src/components/BottomNav.js +18 -18
- package/dist/packages/core/src/components/BulkBar.js +25 -25
- package/dist/packages/core/src/components/Button.js +16 -16
- package/dist/packages/core/src/components/Calendar.js +17 -17
- package/dist/packages/core/src/components/ChartSection.js +30 -30
- package/dist/packages/core/src/components/CountTile.js +38 -38
- package/dist/packages/core/src/components/DatePicker.js +59 -59
- package/dist/packages/core/src/components/Dialog.js +14 -14
- package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.js +8 -8
- package/dist/packages/core/src/components/FilterPanel.js +6 -6
- package/dist/packages/core/src/components/FilterSectionInfo.js +6 -6
- package/dist/packages/core/src/components/GameIntro.js +1 -1
- package/dist/packages/core/src/components/Icon.js +2 -2
- package/dist/packages/core/src/components/ItemActions.js +16 -16
- package/dist/packages/core/src/components/LandingHeader.js +6 -6
- package/dist/packages/core/src/components/LineChart.js +24 -24
- package/dist/packages/core/src/components/Loader.js +5 -5
- package/dist/packages/core/src/components/NoticeBox.js +3 -3
- package/dist/packages/core/src/components/PageTitle.js +2 -2
- package/dist/packages/core/src/components/Pagination.js +4 -4
- package/dist/packages/core/src/components/Panel.js +2 -2
- package/dist/packages/core/src/components/PanelRow.js +6 -13
- package/dist/packages/core/src/components/PassCard/PassCard.js +9 -9
- package/dist/packages/core/src/components/PassCard/passCardProduct.js +2 -2
- package/dist/packages/core/src/components/PaymentPlan.js +5 -5
- package/dist/packages/core/src/components/Popup/ConfirmPopup.js +29 -29
- package/dist/packages/core/src/components/Popup/CouponInputPopup.js +2 -2
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +18 -25
- package/dist/packages/core/src/components/Popup/ListPopup.js +3 -3
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +7 -7
- package/dist/packages/core/src/components/Popup/Popup.js +27 -27
- package/dist/packages/core/src/components/Popup/SaveTablePopup.js +5 -5
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +4 -4
- package/dist/packages/core/src/components/Popup/customerDetailUtils.js +18 -18
- package/dist/packages/core/src/components/Popup/useDiscardGuard.js +4 -4
- package/dist/packages/core/src/components/ProgressIndicator.js +5 -5
- package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +16 -16
- package/dist/packages/core/src/components/Rating.js +20 -20
- package/dist/packages/core/src/components/Report/ReportDocument.js +21 -21
- package/dist/packages/core/src/components/RingChart.js +5 -5
- package/dist/packages/core/src/components/SectionHeadFloat.js +15 -15
- package/dist/packages/core/src/components/SegmentedControl.js +34 -34
- package/dist/packages/core/src/components/SideNav.js +37 -37
- package/dist/packages/core/src/components/Slider.js +2 -2
- package/dist/packages/core/src/components/SoundCompass.js +5 -5
- package/dist/packages/core/src/components/Table.js +257 -98
- package/dist/packages/core/src/components/Tabs.js +12 -12
- package/dist/packages/core/src/components/TextField.js +34 -34
- package/dist/packages/core/src/components/Toast.js +6 -6
- package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +4 -4
- package/dist/packages/core/src/components/Tooltip/Tooltip.js +22 -22
- package/dist/packages/core/src/components/Tooltip/TrainingTooltip.js +4 -4
- package/dist/packages/core/src/components/TopBanner.js +28 -28
- package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +11 -11
- package/dist/packages/core/src/components/Training/CategoryIcon.js +2 -2
- package/dist/packages/core/src/components/Training/NoisePlayerContext.js +12 -12
- package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingBlock.js +11 -11
- package/dist/packages/core/src/components/Training/TrainingBlockButton.js +10 -10
- package/dist/packages/core/src/components/Training/TrainingBlockGrid.js +21 -21
- package/dist/packages/core/src/components/Training/TrainingCard.css +13 -1
- package/dist/packages/core/src/components/Training/TrainingCard.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingCardGroup.js +6 -6
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +81 -81
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +17 -17
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +127 -127
- package/dist/packages/core/src/components/Training/TrainingStepCard.js +18 -18
- package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +4 -4
- package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +12 -12
- package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +12 -12
- package/dist/packages/core/src/components/Training/TrainingStepTitle.js +4 -4
- package/dist/packages/core/src/components/Training/TrainingWordCard.js +17 -17
- package/dist/packages/core/src/components/Training/WordTablePopup.js +5 -5
- package/dist/packages/core/src/components/Training/audioPlaybackRules.js +8 -8
- package/dist/packages/core/src/components/Training/audioTimeline.js +25 -25
- package/dist/packages/core/src/components/Training/oneShotSound.js +13 -13
- package/dist/packages/core/src/components/Training/trainingMissStats.js +20 -20
- package/dist/packages/core/src/components/Training/trainingPopupUtils.js +7 -7
- package/dist/packages/core/src/components/Training/useAudioPreview.js +15 -15
- package/dist/packages/core/src/components/Training/useFloatingFocus.js +14 -14
- package/dist/packages/core/src/components/Training/useNoiseFade.js +22 -22
- package/dist/packages/core/src/components/Training/useObjectUrls.js +2 -2
- package/dist/packages/core/src/components/Training/usePlaybackSequencer.d.ts +2 -2
- package/dist/packages/core/src/components/Training/usePlaybackSequencer.js +23 -23
- package/dist/packages/core/src/components/Training/useProgressTicker.js +33 -33
- package/dist/packages/core/src/components/Training/useTTSPlayback.js +17 -17
- package/dist/packages/core/src/components/Training/useWaveReady.js +11 -11
- package/dist/packages/core/src/components/UpdateAlert.js +6 -6
- package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +7 -1
- package/dist/packages/core/src/components/Word/TableFilterPanel.js +3 -3
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +10 -10
- package/dist/packages/core/src/components/Word/WordTableBlock.js +28 -28
- package/dist/packages/core/src/components/Word/WordTableButton.css +16 -2
- package/dist/packages/core/src/components/Word/WordTableThumbnail.css +6 -1
- package/dist/packages/core/src/components/Word/generatedDraft.js +14 -14
- package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -7
- package/dist/packages/core/src/components/Word/wordScoreStats.js +15 -15
- package/dist/packages/core/src/components/Word/wordTablePrint.js +10 -10
- package/dist/packages/core/src/components/Word/wordTableRecommend.js +11 -11
- package/dist/packages/core/src/components/Word/wordTableReview.js +28 -28
- package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +8 -11
- package/dist/packages/core/src/components/Word/wordTableSettings.js +48 -36
- package/dist/packages/core/src/components/WordTable.js +130 -130
- package/dist/packages/core/src/components/barChartData.d.ts +16 -1
- package/dist/packages/core/src/components/barChartData.js +49 -29
- package/dist/packages/core/src/components/barChartTypes.js +8 -8
- package/dist/packages/core/src/components/tableTrackList.js +11 -11
- package/dist/packages/core/src/components/updateAlertStack.js +28 -28
- package/dist/packages/core/src/components/useBarChartControl.js +4 -6
- package/dist/packages/core/src/components/useComboboxSuggestions.js +17 -17
- package/dist/packages/core/src/components/useToastLifecycle.js +18 -18
- package/dist/packages/core/src/components/wordTableLayout.d.ts +17 -4
- package/dist/packages/core/src/components/wordTableLayout.js +5 -5
- package/dist/packages/core/src/context/I18nProvider.js +6 -6
- package/dist/packages/core/src/hooks/focusReturn.js +27 -27
- package/dist/packages/core/src/hooks/inputModality.js +13 -13
- package/dist/packages/core/src/hooks/touchToggle.js +39 -39
- package/dist/packages/core/src/hooks/useAnchoredMenu.js +30 -30
- package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +28 -28
- package/dist/packages/core/src/hooks/useDocumentTitle.js +10 -10
- package/dist/packages/core/src/hooks/useEditableList.js +4 -4
- package/dist/packages/core/src/hooks/useFocusTrap.js +2 -2
- package/dist/packages/core/src/hooks/useFontScale.js +6 -6
- package/dist/packages/core/src/hooks/useIcon.js +7 -7
- package/dist/packages/core/src/hooks/useListControls.js +2 -2
- package/dist/packages/core/src/hooks/usePageResetOnChange.js +3 -3
- package/dist/packages/core/src/hooks/usePasswordChange.js +4 -4
- package/dist/packages/core/src/hooks/usePendingPress.js +7 -7
- package/dist/packages/core/src/hooks/useRelativeDay.js +4 -4
- package/dist/packages/core/src/hooks/useSoundCompassExam.js +6 -6
- package/dist/packages/core/src/hooks/useTablePaging.d.ts +0 -11
- package/dist/packages/core/src/hooks/useTablePaging.js +29 -34
- package/dist/packages/core/src/hooks/useTrainingSessionLog.js +35 -35
- package/dist/packages/core/src/hooks/useVisitSchedule.js +4 -4
- package/dist/packages/core/src/hooks/useWordAttributeFilter.js +14 -14
- package/dist/packages/core/src/hooks/useWordMeanings.js +5 -5
- package/dist/packages/core/src/hooks/useWordTrainingFilter.js +7 -7
- package/dist/packages/core/src/i18n/locales/en.json +13 -2
- package/dist/packages/core/src/i18n/locales/ko.json +12 -1
- package/dist/packages/core/src/types/wordSearch.d.ts +9 -0
- package/dist/packages/core/src/utils/audioMeter.js +5 -5
- package/dist/packages/core/src/utils/audioPeaks.d.ts +1 -1
- package/dist/packages/core/src/utils/audioPeaks.js +18 -18
- package/dist/packages/core/src/utils/blockGrid.js +4 -4
- package/dist/packages/core/src/utils/chunkSkew.js +9 -9
- package/dist/packages/core/src/utils/clipboard.js +3 -3
- package/dist/packages/core/src/utils/dailySelectionUtils.js +12 -12
- package/dist/packages/core/src/utils/date.js +12 -12
- package/dist/packages/core/src/utils/devWarn.js +4 -4
- package/dist/packages/core/src/utils/deviceKind.d.ts +37 -7
- package/dist/packages/core/src/utils/deviceKind.js +53 -10
- package/dist/packages/core/src/utils/errorMessage.js +7 -7
- package/dist/packages/core/src/utils/filterPanelSummary.js +14 -14
- package/dist/packages/core/src/utils/format.js +4 -4
- package/dist/packages/core/src/utils/getTrainingData.js +9 -9
- package/dist/packages/core/src/utils/imageConfig.js +17 -17
- package/dist/packages/core/src/utils/koreanSyllable.js +18 -18
- package/dist/packages/core/src/utils/navScroll.js +2 -2
- package/dist/packages/core/src/utils/phonemeConvert.d.ts +7 -0
- package/dist/packages/core/src/utils/phonemeConvert.js +19 -10
- package/dist/packages/core/src/utils/phonemeGroups.js +2 -2
- package/dist/packages/core/src/utils/phonemePairSearch.d.ts +1 -0
- package/dist/packages/core/src/utils/phonemePairSearch.js +118 -102
- package/dist/packages/core/src/utils/phonemeSectionCodes.d.ts +52 -0
- package/dist/packages/core/src/utils/phonemeSectionCodes.js +76 -0
- package/dist/packages/core/src/utils/prefetchChunk.js +6 -6
- package/dist/packages/core/src/utils/safeHref.js +4 -4
- package/dist/packages/core/src/utils/savedWordTables.js +5 -5
- package/dist/packages/core/src/utils/speak.js +12 -12
- package/dist/packages/core/src/utils/textSort.d.ts +0 -10
- package/dist/packages/core/src/utils/textSort.js +38 -48
- package/dist/packages/core/src/utils/theme.js +3 -3
- package/dist/packages/core/src/utils/trainingItemResults.js +3 -3
- package/dist/packages/core/src/utils/ttsConfig.js +2 -2
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +45 -45
- package/dist/packages/core/src/utils/wordContrast.js +3 -3
- package/dist/packages/core/src/utils/wordPairIndex.js +4 -4
- package/dist/packages/core/src/utils/wordTrainingGenerators.js +4 -4
- package/dist/packages/core/src/utils/wordTrainingPool.js +15 -15
- package/dist/packages/core/src/utils/wordTrainingRuntime.js +6 -6
- package/dist/packages/core/src/utils/wornDate.d.ts +3 -3
- package/dist/packages/core/src/utils/wornDate.js +14 -14
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +4 -4
- 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
|
|
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 =
|
|
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
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
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 =
|
|
69
|
+
const prevKey = monthKey(prev.getFullYear(), prev.getMonth());
|
|
70
70
|
if (!byMonth.has(prevKey))
|
|
71
71
|
byMonth.set(prevKey, []);
|
|
72
72
|
}
|
|
73
|
-
const
|
|
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
|
-
|
|
99
|
+
cells.set(key, { year, month, daysInMonth, partsLen, hasPartsB, dayA, dayB, dayParts, dayPartsB });
|
|
100
100
|
}
|
|
101
101
|
/** 그 날이 오늘 뒤인가 — 다음 달 날을 이어 붙일 때 미래는 세지 않는다 */
|
|
102
|
-
const
|
|
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,
|
|
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
|
|
108
|
+
const nextCell = cells.get(monthKey(month === 11 ? year + 1 : year, (month + 1) % 12));
|
|
109
109
|
/* 마지막 주가 다음 달 계열을 더 많이 가질 수 있다 — 그 주만 넓힌다 */
|
|
110
|
-
const 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 &&
|
|
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
|
|
129
|
-
const wLen =
|
|
130
|
-
const wHasB =
|
|
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 <=
|
|
145
|
-
if (
|
|
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 +=
|
|
148
|
-
b +=
|
|
149
|
-
for (let k = 0; k <
|
|
150
|
-
wParts[k] +=
|
|
151
|
-
wPartsB[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
|
|
156
|
-
weekly.push({ label: `${weekly.length + 1}주`, sublabel: `[${start}일~${
|
|
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
|
-
|
|
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
|
|
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,
|
|
11
|
-
fromYmd: ymdOf(y, m0,
|
|
12
|
-
toYmd: ymdOf(y, m0,
|
|
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
|
|
27
|
-
return
|
|
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
|
|
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
|
|
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) &&
|
|
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
|
-
|
|
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
|
|
11
|
-
let
|
|
10
|
+
const offsets = new Map();
|
|
11
|
+
let observer = null;
|
|
12
12
|
const GAP = 12;
|
|
13
13
|
/* fixed 의 기준이 되는 조상 — transform·filter·perspective·contain 이 걸린 가장 가까운 조상, 없으면 문서 */
|
|
14
|
-
function
|
|
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
|
|
24
|
-
for (const el of
|
|
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 =
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
let
|
|
35
|
-
for (const el of
|
|
36
|
-
if (
|
|
37
|
-
el.style.setProperty('--update-alert-stack-offset', `${
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
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 = (
|
|
57
|
+
const n = (offsets.get(el) ?? 1) - 1;
|
|
58
58
|
if (n > 0) {
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
offsets.set(el, n);
|
|
60
|
+
restack();
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
|
-
|
|
64
|
-
|
|
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,
|
|
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
|
|
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
|
|
30
|
-
|
|
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
|
|
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
|
|
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 =>
|
|
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 [
|
|
39
|
-
const
|
|
40
|
-
const filtered =
|
|
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
|
-
|
|
43
|
-
const handleCompositionStart = useCallback(() =>
|
|
44
|
-
const handleCompositionEnd = useCallback(() =>
|
|
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
|
|
47
|
+
const initialActiveIdx = autoActiveFirst ? 0 : -1;
|
|
48
48
|
useEffect(() => { if (!open)
|
|
49
|
-
setActiveIdx(
|
|
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 :
|
|
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?.(
|
|
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 :
|
|
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(
|
|
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
|
|
25
|
-
const
|
|
26
|
-
const
|
|
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
|
|
33
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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 (
|
|
57
|
+
if (pausedRef.current || !hideTimerRef.current)
|
|
58
58
|
return;
|
|
59
|
-
|
|
59
|
+
pausedRef.current = true;
|
|
60
60
|
clearTimeout(hideTimerRef.current);
|
|
61
61
|
hideTimerRef.current = null;
|
|
62
|
-
|
|
62
|
+
remainingRef.current = Math.max(0, remainingRef.current - (Date.now() - startedAtRef.current));
|
|
63
63
|
}, []);
|
|
64
64
|
const resume = useCallback(() => {
|
|
65
|
-
if (
|
|
65
|
+
if (!pausedRef.current)
|
|
66
66
|
return;
|
|
67
|
-
|
|
68
|
-
|
|
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 (
|
|
92
|
-
|
|
91
|
+
if (pausedRef.current)
|
|
92
|
+
remainingRef.current = DURATION_MS;
|
|
93
93
|
else
|
|
94
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
7
|
-
const
|
|
8
|
-
if (
|
|
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
|
|
11
|
-
return
|
|
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 =
|
|
17
|
+
const code = supportedCode(locale);
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
if (i18n.language !== code) {
|
|
20
20
|
i18n.changeLanguage(code);
|