@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.
- package/CHANGELOG.md +617 -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 +108 -66
- 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/HearingDeviceInfo.d.ts +5 -5
- package/dist/packages/core/src/components/HearingDeviceInfo.js +2 -2
- 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.d.ts +2 -4
- package/dist/packages/core/src/components/PanelRow.js +8 -15
- 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/DeviceListPopup.d.ts +4 -8
- package/dist/packages/core/src/components/Popup/DeviceListPopup.js +1 -1
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +19 -26
- package/dist/packages/core/src/components/Popup/ListPopup.js +6 -6
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +10 -10
- 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.d.ts +7 -4
- 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 +12 -12
- 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 +83 -83
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +17 -17
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +128 -128
- 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 +13 -13
- 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.d.ts +1 -1
- package/dist/packages/core/src/components/Training/oneShotSound.js +14 -14
- package/dist/packages/core/src/components/Training/stepAnswerable.d.ts +2 -2
- package/dist/packages/core/src/components/Training/trainingMissStats.js +22 -22
- 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 +18 -18
- 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 +17 -17
- package/dist/packages/core/src/components/Word/wordTablePrint.js +10 -10
- package/dist/packages/core/src/components/Word/wordTableRecommend.js +13 -13
- 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 +22 -7
- 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.d.ts +1 -1
- package/dist/packages/core/src/hooks/inputModality.js +14 -14
- 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/useDeviceManagement.d.ts +5 -17
- package/dist/packages/core/src/hooks/useDeviceManagement.js +20 -8
- 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.d.ts +3 -3
- package/dist/packages/core/src/hooks/usePageResetOnChange.js +6 -6
- 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 +7 -7
- package/dist/packages/core/src/hooks/useTablePaging.js +40 -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.d.ts +3 -1
- package/dist/packages/core/src/hooks/useWordTrainingFilter.js +14 -12
- 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.d.ts +2 -2
- package/dist/packages/core/src/utils/devWarn.js +7 -7
- 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.d.ts +1 -1
- package/dist/packages/core/src/utils/filterPanelSummary.js +16 -16
- 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.d.ts +2 -2
- package/dist/packages/core/src/utils/navScroll.js +6 -6
- 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/pressable.d.ts +1 -1
- package/dist/packages/core/src/utils/pressable.js +3 -3
- 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/sessionTab.d.ts +7 -7
- package/dist/packages/core/src/utils/sessionTab.js +9 -9
- package/dist/packages/core/src/utils/speak.js +12 -12
- package/dist/packages/core/src/utils/textSort.d.ts +5 -5
- package/dist/packages/core/src/utils/textSort.js +47 -47
- 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.d.ts +2 -2
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +48 -48
- 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
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
|
39
|
-
const
|
|
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;
|
|
42
|
+
for (let n = 2; used.has(id); n++)
|
|
43
43
|
id = `${it.id}~${n}`;
|
|
44
|
-
|
|
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
|
|
81
|
+
const showBand = Boolean(brand?.show);
|
|
82
82
|
const [showTopInfo, setShowTopInfo] = useState(true);
|
|
83
83
|
const [showBottomInfo, setShowBottomInfo] = useState(true);
|
|
84
|
-
const
|
|
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', '페이지 번호')]:
|
|
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
|
-
?
|
|
233
|
-
id:
|
|
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);
|
|
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
|
|
320
|
-
let id =
|
|
321
|
-
for (let n = 2;
|
|
322
|
-
id = `${
|
|
323
|
-
onAddItem({ id, words: [
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
13
|
+
const seen = new Set();
|
|
14
14
|
for (const v of values) {
|
|
15
15
|
const key = keyOf(v);
|
|
16
16
|
/* 같은 열쇠가 두 번 오면 한 번만 담는다 — 초안에 같은 줄이 둘이면 고정·빼기가
|
|
17
17
|
어느 것에 걸린 것인지 사람이 알 수 없다. */
|
|
18
|
-
if (
|
|
18
|
+
if (seen.has(key))
|
|
19
19
|
continue;
|
|
20
|
-
|
|
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
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
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 (
|
|
53
|
+
if (fresh.length >= slots)
|
|
54
54
|
break; // 정확성이 아니라 효율 — 위 주석 참고
|
|
55
55
|
const key = keyOf(v);
|
|
56
|
-
if (
|
|
56
|
+
if (seen.has(key))
|
|
57
57
|
continue;
|
|
58
|
-
|
|
59
|
-
|
|
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 :
|
|
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,
|
|
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
|
|
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
|
-
...(
|
|
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', '페이지 번호')]:
|
|
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 {
|
|
1
|
+
import { compareKorean } from '../../utils/textSort';
|
|
2
2
|
import { labelOfCellKey } from './wordTableReview';
|
|
3
3
|
/** △ 를 반 점으로 치는 셈 — 저장할 때 서버가 쓰는 규칙과 같다. */
|
|
4
|
-
function
|
|
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
|
|
21
|
+
const byKey = new Map();
|
|
22
22
|
/* 최근 것을 알려면 시간순으로 훑어야 한다. 서버는 최신순으로 주지만 여기서 다시
|
|
23
23
|
정렬한다 — 부르는 쪽의 차례에 기대면 그쪽이 바뀔 때 조용히 어긋난다. */
|
|
24
|
-
const
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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 ||
|
|
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
|
|
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 =
|
|
63
|
+
const cur = byKey.get(key) ?? { tableName: r.tableName, tableId: r.tableId, rows: [] };
|
|
64
64
|
cur.rows.push(r);
|
|
65
|
-
|
|
65
|
+
byKey.set(key, cur);
|
|
66
66
|
}
|
|
67
67
|
const out = [];
|
|
68
|
-
for (const v of
|
|
68
|
+
for (const v of byKey.values()) {
|
|
69
69
|
/* 매긴 칸이 하나도 없는 기록은 흐름에서 뺀다 — 정답률 0%로 넣으면 [0, 0.5] 처럼 나아진 적 없는 표가 +50%로 읽혔다
|
|
70
70
|
(summarize 는 같은 경우를 null 로 따로 둔다). 모두 비었으면 그 표는 흐름이 없다. */
|
|
71
|
-
const
|
|
72
|
-
const rows = v.rows.filter(r =>
|
|
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
|
|
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
|
|
21
|
+
let restoreTitle = null;
|
|
22
22
|
/* 진행 중인 인쇄 — 그림을 기다리는(최대 3초) 사이 다시 누르면 숨김 iframe 이 둘 생겨 인쇄 대화상자가 두 번 떴다(autoPrint 대기 중 '인쇄').
|
|
23
23
|
afterprint 가 안 오는 브라우저에서 영영 막히지 않게 10초가 지나면 새 인쇄를 받는다. */
|
|
24
|
-
let
|
|
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 (
|
|
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 (
|
|
50
|
-
|
|
49
|
+
if (restoreTitle === restore)
|
|
50
|
+
restoreTitle = null;
|
|
51
51
|
};
|
|
52
|
-
|
|
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
|
-
|
|
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 (
|
|
70
|
-
|
|
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 {
|
|
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
|
|
27
|
-
if (
|
|
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
|
|
33
|
+
const coveredSet = new Set();
|
|
34
34
|
const covered = [];
|
|
35
35
|
for (const label of t.cellLabels) {
|
|
36
|
-
if (
|
|
37
|
-
|
|
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
|
-
||
|
|
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
|
|
61
|
-
return weakLabels.filter(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
|
|
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 (
|
|
82
|
-
|
|
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 =
|
|
87
|
+
const cell = found.get(label);
|
|
88
88
|
if (cell !== undefined)
|
|
89
89
|
out.push(cell);
|
|
90
90
|
}
|