@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
|
@@ -20,7 +20,7 @@ import { isTouchPointer, recentlyTouched, useTouchToggle } from '../hooks/touchT
|
|
|
20
20
|
|
|
21
21
|
ihab 은 **흰 판을 마스크로 쓴다**(사용자 지시 2026-09-30: 글자색을 입힌다) — 그쪽은
|
|
22
22
|
`WordTable.css` 가 제 곁의 파일을 직접 가리키므로 여기서 안 들여온다. */
|
|
23
|
-
import
|
|
23
|
+
import speechearLogo from '../logo/Logo_speechear_kr.svg';
|
|
24
24
|
import { WORD_TABLE_PAPER, gridFor, CUT_CLASS, posLabels, wordTypeLabels, expandSlashLabel, dotDashStyle, REF_W_BOTH, REF_W_TEXT_ONLY, REF_W_IMAGE_ONLY, REF_H_BOTH, REF_H_TEXT_ONLY, REF_H_IMAGE_ONLY, } from './wordTableLayout';
|
|
25
25
|
import { useFitScale } from './useFitScale';
|
|
26
26
|
import { claimSound } from './Training/currentSound';
|
|
@@ -32,7 +32,7 @@ import { playOneShot } from './Training/oneShotSound';
|
|
|
32
32
|
(상수 하나는 남긴다 — 규칙이 상수는 허용하고, 지우면 공개 API 가 바뀐다.) */
|
|
33
33
|
export { WORD_TABLE_PAPER } from './wordTableLayout';
|
|
34
34
|
/** 눈에는 안 보이고 낭독기에는 읽히는 글자(ProgressIndicator 와 같은 규칙) */
|
|
35
|
-
const
|
|
35
|
+
const srOnly = {
|
|
36
36
|
position: 'absolute', width: 1, height: 1, margin: -1, padding: 0, overflow: 'hidden',
|
|
37
37
|
clipPath: 'inset(50%)', whiteSpace: 'nowrap', border: 0,
|
|
38
38
|
};
|
|
@@ -42,9 +42,9 @@ const 보이지않게 = {
|
|
|
42
42
|
* 300~500ms 가 흔한 값이다(iOS 의 길게 누르기는 500ms, Android 는 400ms 안팎).
|
|
43
43
|
* 짧으면 살짝 스친 손가락에 칸이 끌려가고, 길면 "안 되네" 하고 손을 뗀다.
|
|
44
44
|
*/
|
|
45
|
-
const
|
|
45
|
+
const LONG_PRESS_MS = 400;
|
|
46
46
|
/** 길게 누르기가 익기 전에 이만큼 움직이면 그건 끌기가 아니라 **스크롤**이라 취소한다 */
|
|
47
|
-
const
|
|
47
|
+
const MOVE_THRESHOLD_PX = 8;
|
|
48
48
|
function WordCell({ label, subLabel, colloquialLabel, homonum, showWord = true, showImage = true, showPronunciation = true, showColloquial = true, buttonSize = 40, wordInfo, }) {
|
|
49
49
|
const { t } = useTranslation();
|
|
50
50
|
const outerRef = useRef(null);
|
|
@@ -54,7 +54,7 @@ function WordCell({ label, subLabel, colloquialLabel, homonum, showWord = true,
|
|
|
54
54
|
const [playing, setPlaying] = useState(false);
|
|
55
55
|
/* 받은 주소에 **어느 낱말의 것인지** 를 함께 둔다 — 주소만 들고 있으면 같은 칸의 낱말이 바뀐 뒤 새 주소를 받는 동안(또는
|
|
56
56
|
못 받으면) 누를 때 앞 낱말 소리가 났다(틀린 자극). 지금 낱말의 것일 때만 쓴다. */
|
|
57
|
-
const [
|
|
57
|
+
const [fetchedAudio, setFetchedAudio] = useState({ for: '', url: '' });
|
|
58
58
|
const [infoAnchor, setInfoAnchor] = useState(null);
|
|
59
59
|
// 발음(subLabel)이 없으면 단어로 표준발음 조회(앱 주입 resolver)
|
|
60
60
|
const pron = (subLabel && subLabel.length > 0) ? subLabel : (getPronunciationResolver()?.(label, homonum) ?? '');
|
|
@@ -63,7 +63,7 @@ function WordCell({ label, subLabel, colloquialLabel, homonum, showWord = true,
|
|
|
63
63
|
const handleInfoEnter = useCallback(() => setInfoAnchor(infoRef.current?.getBoundingClientRect() ?? null), []);
|
|
64
64
|
const handleInfoLeave = useCallback(() => setInfoAnchor(null), []);
|
|
65
65
|
/* 터치 — 톡하면 열렸다 떼자마자 닫혀 낱말 정보를 읽을 수 없었다. 톡으로 열고 닫는다(마우스·키보드는 그대로) */
|
|
66
|
-
const
|
|
66
|
+
const infoToggle = useTouchToggle(!!infoAnchor, (next) => { if (next)
|
|
67
67
|
handleInfoEnter();
|
|
68
68
|
else
|
|
69
69
|
handleInfoLeave(); }, infoRef);
|
|
@@ -83,8 +83,8 @@ function WordCell({ label, subLabel, colloquialLabel, homonum, showWord = true,
|
|
|
83
83
|
arr.push(t('wordTable.info.wordType', { v: expandSlashLabel(wordInfo.wordType, wordTypeLabels(t)), defaultValue: '유형: {{v}}' }));
|
|
84
84
|
return arr.length ? arr : null;
|
|
85
85
|
}, [wordInfo, t]);
|
|
86
|
-
const
|
|
87
|
-
const audioUrl =
|
|
86
|
+
const audioKey = pron || label;
|
|
87
|
+
const audioUrl = fetchedAudio.for === audioKey ? fetchedAudio.url : '';
|
|
88
88
|
useEffect(() => {
|
|
89
89
|
const resolver = getTTSUrlResolver();
|
|
90
90
|
const ttsText = pron || label;
|
|
@@ -92,7 +92,7 @@ function WordCell({ label, subLabel, colloquialLabel, homonum, showWord = true,
|
|
|
92
92
|
return;
|
|
93
93
|
let cancelled = false;
|
|
94
94
|
Promise.resolve(resolver(ttsText)).then(url => { if (!cancelled && url)
|
|
95
|
-
|
|
95
|
+
setFetchedAudio({ for: ttsText, url }); });
|
|
96
96
|
return () => { cancelled = true; };
|
|
97
97
|
}, [pron, label]);
|
|
98
98
|
// 컴포넌트 언마운트 시 발음 재생 중단 — audioRef는 <audio> JSX가 아니라 new Audio()로
|
|
@@ -122,11 +122,11 @@ function WordCell({ label, subLabel, colloquialLabel, homonum, showWord = true,
|
|
|
122
122
|
/* 끝나거나 멈추면 **그 자리에서** 자리를 비운다 — 약속으로 받으면 한 틱 늦어, 남이
|
|
123
123
|
자물쇠를 가져간 순간에 이 칸이 아직 "재생 중" 으로 보인다.
|
|
124
124
|
이미 다음 사람이 가져갔으면 release 는 아무것도 안 한다. */
|
|
125
|
-
const
|
|
126
|
-
audioRef.current =
|
|
127
|
-
stopRef.current =
|
|
125
|
+
const sound = playOneShot(audioUrl, () => { setPlaying(false); releaseRef.current?.(); });
|
|
126
|
+
audioRef.current = sound.el;
|
|
127
|
+
stopRef.current = sound.stop;
|
|
128
128
|
/* 자물쇠를 잡으면 앞서 울리던 것(다른 칸·훈련 카드 등)이 멈추고, 남이 잡으면 stop 이 불려 내가 멈춘다 */
|
|
129
|
-
releaseRef.current = claimSound(
|
|
129
|
+
releaseRef.current = claimSound(sound.stop);
|
|
130
130
|
}, [playing, audioUrl]);
|
|
131
131
|
const hasImage = showImage && !!imageUrl && !imgFailed;
|
|
132
132
|
const refW = hasImage && showWord ? REF_W_BOTH : hasImage ? REF_W_IMAGE_ONLY : REF_W_TEXT_ONLY;
|
|
@@ -165,7 +165,7 @@ function WordCell({ label, subLabel, colloquialLabel, homonum, showWord = true,
|
|
|
165
165
|
/* 아이콘만 있는 버튼이라 이름이 없으면 화면 낭독기가 "버튼"으로만 읽는다.
|
|
166
166
|
한 표에 칸마다 하나씩 있어 열두 개가 다 "버튼"이 된다 — 어느 낱말의
|
|
167
167
|
것인지 알 길이 없으므로 낱말을 이름에 넣는다. */
|
|
168
|
-
ariaLabel: t('wordTable.wordInfo', '{{word}} 낱말 정보', { word: label }), onClick: (e) => { e.stopPropagation();
|
|
168
|
+
ariaLabel: t('wordTable.wordInfo', '{{word}} 낱말 정보', { word: label }), onClick: (e) => { e.stopPropagation(); infoToggle.touchPress(); } }), infoAnchor && infoTooltipItems && (_jsx(DescriptionTooltip, { anchorRect: infoAnchor, items: infoTooltipItems, showIndex: false }))] }))] }));
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
171
171
|
* 단어표(표 디자인) — A4 종이를 컨테이너에 맞춰 축소(scale)하고,
|
|
@@ -173,7 +173,7 @@ function WordCell({ label, subLabel, colloquialLabel, homonum, showWord = true,
|
|
|
173
173
|
* 표시(items)·편집(editCells) 두 모드를 지원한다.
|
|
174
174
|
* @public
|
|
175
175
|
*/
|
|
176
|
-
export default function WordTable({ title, titleIcon, disc, className, orientation = 'landscape', contentTypes = ['
|
|
176
|
+
export default function WordTable({ title, titleIcon, disc, className, orientation = 'landscape', contentTypes = ['image', 'word', 'standardPron'], cutLine = 'show', showOutline = true, showDivLine = true, items, editCells, count, activeCell = null, onActivateCell, onRemoveCell, onRemoveItem, onReorder, renderCellAction, showFillerAdd = false, contentRef, buttonSize = 40, brand, code, pageNumber, pageNumberPosition = 'right', fitHeight = false, scoring = false, marks, onMarkClick, scoreIndexOffset = 0, }) {
|
|
177
177
|
const clickMark = useCallback((cellIndex, clicked) => {
|
|
178
178
|
onMarkClick?.(scoreIndexOffset + cellIndex, clicked);
|
|
179
179
|
}, [onMarkClick, scoreIndexOffset]);
|
|
@@ -188,7 +188,7 @@ export default function WordTable({ title, titleIcon, disc, className, orientati
|
|
|
188
188
|
_jsx("div", { className: cx('word-table-cell__score', mark ? 'word-table-cell__score--marked' : ''),
|
|
189
189
|
/* CSS 의 가운데 맞춤(translateX(-50%))을 그대로 두고 되돌림만 얹는다. 바닥 가운데를
|
|
190
190
|
기준으로 키워야 칸 아래쪽에 붙은 자리가 흔들리지 않는다. */
|
|
191
|
-
style:
|
|
191
|
+
style: markUnscale > 1 ? { transform: `translateX(-50%) scale(${markUnscale})`, transformOrigin: 'bottom center' } : undefined, children: MARKS.map(m => (_jsx("span", { className: cx('word-table-cell__score-slot', mark === m ? 'word-table-cell__score-slot--on' : ''), children: _jsx(Button, { htmlType: "button", size: buttonSize, type: "dot",
|
|
192
192
|
/* **동그라미다**(말귀 요청 ㊸, 2026-10-01). `Button` 기본은 모서리가 둥근
|
|
193
193
|
네모(`shape="rounded"`)인데, 같은 평가를 말하는 셈과 낱말 목록은
|
|
194
194
|
`CircleBadge` 라 한 창 안에서 **같은 뜻이 두 모양**으로 섰다.
|
|
@@ -197,7 +197,7 @@ export default function WordTable({ title, titleIcon, disc, className, orientati
|
|
|
197
197
|
프롭으로 열지 않는다: 둥근 네모를 원하는 자리가 없고, 열어 두면 **같은
|
|
198
198
|
결정을 자리마다 다시 적어야** 해서 조용히 갈린다(말귀가 두 창에서 실제로
|
|
199
199
|
갈렸다 — 회차 창은 동그라미, 채점 창은 둥근 네모). */
|
|
200
|
-
shape: "pill", icon: MARK_ICON[m], variant: mark === m ? MARK_VARIANT[m] : 'default', selected: mark === m, useBorder: false, ...(
|
|
200
|
+
shape: "pill", icon: MARK_ICON[m], variant: mark === m ? MARK_VARIANT[m] : 'default', selected: mark === m, useBorder: false, ...(canMark ? {} : { tabIndex: -1, role: 'img' }),
|
|
201
201
|
/* **마우스를 올리면 이름이 뜬다**(말귀 요청 ㊴, 2026-10-01). 이 셋은 아이콘만
|
|
202
202
|
있어 낭독기에는 `낙지 맞음` 으로 제대로 읽히는데 **눈으로 보는 사람에게는
|
|
203
203
|
이름이 없었다** — 세모가 '애매' 인지 '반만 맞음' 인지 알 길이 없다.
|
|
@@ -205,14 +205,14 @@ export default function WordTable({ title, titleIcon, disc, className, orientati
|
|
|
205
205
|
얹은 자리에서 알고 싶은 것은 이 표식이 무엇이냐 하나다.
|
|
206
206
|
`ariaLabel` 은 그대로 둔다 — 낭독기는 칸을 안 보고 지나므로 거기서는
|
|
207
207
|
낱말이 있어야 "어느 칸의 맞음" 인지 알 수 있다. 둘이 일부러 다르다. */
|
|
208
|
-
showTooltipInfo: t(`wordTable.scoring.markName.${m}`, { defaultValue: m }), ariaLabel: t(`wordTable.scoring.${m}`, { word: cellLabel, defaultValue: `${cellLabel} ${m}` }), onClick:
|
|
208
|
+
showTooltipInfo: t(`wordTable.scoring.markName.${m}`, { defaultValue: m }), ariaLabel: t(`wordTable.scoring.${m}`, { word: cellLabel, defaultValue: `${cellLabel} ${m}` }), onClick: canMark ? (e) => { e.stopPropagation(); clickMark(cellIndex, m); } : undefined }) }, m))) }));
|
|
209
209
|
};
|
|
210
210
|
const { t } = useTranslation();
|
|
211
211
|
const isEdit = editCells !== undefined;
|
|
212
212
|
/** title·titleIcon·disc 중 하나라도 있으면 Section 으로 감싼다 */
|
|
213
213
|
const wrapped = title != null || !!titleIcon || !!disc;
|
|
214
214
|
/* 틀 높이는 화면 폭에 따라 갈린다 — 훅은 늘 부른다(순서 고정) */
|
|
215
|
-
const
|
|
215
|
+
const isNarrow = useMediaQuery('(max-width: 767px)');
|
|
216
216
|
// 드래그 상태(표시 모드 재정렬)
|
|
217
217
|
const dragIndex = useRef(null);
|
|
218
218
|
const [dragOver, setDragOver] = useState(null);
|
|
@@ -221,171 +221,171 @@ export default function WordTable({ title, titleIcon, disc, className, orientati
|
|
|
221
221
|
바꿀 길이 아예 없었고(끌 수 있다는 것조차 알 수 없다), 터치 기기에서는 HTML5 DnD 가
|
|
222
222
|
굴러가지도 않아 손가락으로도 못 바꿨다. 표를 만드는 화면의 핵심 조작인데
|
|
223
223
|
마우스 있는 사람만 쓸 수 있었다. 마우스 끌기는 그대로 두고 두 길을 더한다. */
|
|
224
|
-
const
|
|
224
|
+
const wordCount = items?.length ?? 0;
|
|
225
225
|
/** 지금 손가락에 끌려가는 칸 */
|
|
226
|
-
const [
|
|
226
|
+
const [dragging, setDragging] = useState(null);
|
|
227
227
|
/** 낭독기에 바뀐 자리를 알리는 글. 처음에는 비워 둔다 — 열자마자 뜬금없이 읽지 않게. */
|
|
228
|
-
const [
|
|
228
|
+
const [moveAnnounce, setMoveAnnounce] = useState('');
|
|
229
229
|
/** 다시 그린 뒤 초점을 둘 칸 번호 — 옮긴 칸을 초점이 따라가야 다음 Alt+화살표가 같은 칸을 민다 */
|
|
230
|
-
const
|
|
230
|
+
const focusTarget = useRef(null);
|
|
231
231
|
/** 칸 격자 DOM — 손가락 밑의 칸을 찾고, 옮긴 뒤 손잡이에 초점을 주는 데 쓴다 */
|
|
232
|
-
const
|
|
232
|
+
const gridRef = useRef(null);
|
|
233
233
|
/** 지금 떨어뜨릴 자리(상태와 같은 값의 ref) — pointerup 은 마지막 렌더를 기다려 주지 않는다 */
|
|
234
|
-
const
|
|
234
|
+
const dropIndex = useRef(null);
|
|
235
235
|
/** 길게 누르기가 걸어 둔 것들을 한 번에 푸는 함수 */
|
|
236
|
-
const
|
|
236
|
+
const release = useRef(null);
|
|
237
237
|
/** `contentRef`(인쇄·캡처용, 부르는 쪽 것)와 우리 `격자` 를 함께 채운다 */
|
|
238
|
-
const
|
|
239
|
-
|
|
238
|
+
const gridContentRef = useCallback((el) => {
|
|
239
|
+
gridRef.current = el;
|
|
240
240
|
if (typeof contentRef === 'function')
|
|
241
241
|
contentRef(el);
|
|
242
242
|
else if (contentRef)
|
|
243
243
|
contentRef.current = el;
|
|
244
244
|
}, [contentRef]);
|
|
245
|
-
const
|
|
246
|
-
|
|
245
|
+
const move = useCallback((from, to) => {
|
|
246
|
+
focusTarget.current = to;
|
|
247
247
|
onReorder?.(from, to);
|
|
248
|
-
|
|
249
|
-
n: to + 1, total:
|
|
248
|
+
setMoveAnnounce(t('wordTable.reorder.moved', {
|
|
249
|
+
n: to + 1, total: wordCount, defaultValue: '{{total}}칸 가운데 {{n}}번째로 옮김',
|
|
250
250
|
}));
|
|
251
|
-
}, [onReorder, t,
|
|
251
|
+
}, [onReorder, t, wordCount]);
|
|
252
252
|
/* 옮긴 칸을 초점이 따라간다. 자리를 잃으면 다음 Alt+화살표가 엉뚱한 칸을 민다 —
|
|
253
253
|
아예 문서 처음으로 튀어 어디였는지도 알 수 없다. 의존성 배열 없이 매 렌더 뒤에
|
|
254
254
|
보되 `초점갈곳` 이 비어 있으면 곧바로 돌아온다(거의 언제나 그렇다). */
|
|
255
255
|
useEffect(() => {
|
|
256
|
-
const to =
|
|
256
|
+
const to = focusTarget.current;
|
|
257
257
|
if (to == null)
|
|
258
258
|
return;
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
259
|
+
focusTarget.current = null;
|
|
260
|
+
const cells = gridRef.current?.querySelectorAll('.word-table-block__card:not(.word-table-block__card--filler)');
|
|
261
|
+
cells?.[to]?.querySelector('.word-table-block__card-handle button')?.focus();
|
|
262
262
|
});
|
|
263
263
|
/* 떠날 때 걸어 둔 것을 푼다 — 길게 누르는 중에 팝업이 닫히면 window 리스너와
|
|
264
264
|
타이머가 남아 사라진 화면의 상태를 건드린다(표의 폭 조절에서 겪은 것과 같다). */
|
|
265
|
-
useEffect(() => () => {
|
|
265
|
+
useEffect(() => () => { release.current?.(); }, []);
|
|
266
266
|
/**
|
|
267
267
|
* **키보드로 옮긴다** — 손잡이에 초점을 두고 `Alt`+`←`/`→`.
|
|
268
268
|
*
|
|
269
269
|
* 낱말 카드 안 다른 단추(재생·정보·빼기)는 Enter·Space 로 눌리고, 화살표만으로는
|
|
270
270
|
* 아무 일도 안 일어난다. `Alt` 를 얹어 그 둘 어느 쪽과도 부딪히지 않게 했다.
|
|
271
271
|
*/
|
|
272
|
-
const
|
|
272
|
+
const moveByKey = useCallback((e, index) => {
|
|
273
273
|
if (!onReorder)
|
|
274
274
|
return;
|
|
275
275
|
if (!e.altKey || e.ctrlKey || e.metaKey)
|
|
276
276
|
return;
|
|
277
|
-
const
|
|
278
|
-
if (
|
|
277
|
+
const forward = e.key === 'ArrowRight';
|
|
278
|
+
if (!forward && e.key !== 'ArrowLeft')
|
|
279
279
|
return;
|
|
280
280
|
e.preventDefault();
|
|
281
|
-
const to = index + (
|
|
282
|
-
if (to < 0 || to >=
|
|
281
|
+
const to = index + (forward ? 1 : -1);
|
|
282
|
+
if (to < 0 || to >= wordCount)
|
|
283
283
|
return; // 양 끝 — 아무 일도 하지 않는다
|
|
284
|
-
|
|
285
|
-
}, [onReorder,
|
|
284
|
+
move(index, to);
|
|
285
|
+
}, [onReorder, wordCount, move]);
|
|
286
286
|
/**
|
|
287
287
|
* **손가락으로 옮긴다** — 길게 누르면(400ms) 끌기가 시작되고 손가락 밑의 칸으로 떨어진다.
|
|
288
288
|
*
|
|
289
289
|
* 마우스는 손대지 않는다. 기존 HTML5 드래그 앤 드롭이 그대로 맡는다 —
|
|
290
290
|
* 두 길이 같은 포인터를 두고 다투면 끌기가 중간에 끊긴다.
|
|
291
291
|
*/
|
|
292
|
-
const
|
|
292
|
+
const longPressDrag = useCallback((e, index) => {
|
|
293
293
|
if (!onReorder || e.pointerType === 'mouse')
|
|
294
294
|
return;
|
|
295
295
|
/* 칸 안 단추를 길게 누른 것은 그 단추 몫이다(재생·정보·빼기) — 옮기기 손잡이만 예외.
|
|
296
296
|
안 가르면 발음을 들으려고 꾹 누른 손가락에 칸이 끌려간다. */
|
|
297
|
-
const
|
|
298
|
-
if (
|
|
297
|
+
const pressedButton = e.target.closest?.('button');
|
|
298
|
+
if (pressedButton && !pressedButton.closest('.word-table-block__card-handle'))
|
|
299
299
|
return;
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
let
|
|
300
|
+
release.current?.();
|
|
301
|
+
const start = { x: e.clientX, y: e.clientY };
|
|
302
|
+
let armed = false;
|
|
303
303
|
/* 끌기 중에는 화면이 따라 스크롤되면 안 된다. React 가 다는 touchmove 는 passive 라
|
|
304
304
|
거기서는 막을 수 없어, 익은 순간 직접 단다(passive:false). */
|
|
305
|
-
const
|
|
305
|
+
const blockScroll = (ev) => { if (armed && ev.cancelable)
|
|
306
306
|
ev.preventDefault(); };
|
|
307
307
|
/* **길이 둘이라 갈라 둔다**(2026-10-10, 코어 자체 조사). *문턱을 넘었나* 는 **안 미룬다** — 그 판정이
|
|
308
308
|
길게누름을 거두는 일이라, 한 틀 미루면 손가락을 움직였는데도 끌기가 익어 버린다. *밑에 어느 칸인가*
|
|
309
309
|
는 `elementFromPoint`(레이아웃을 읽는다) + 상태 바꾸기라 **틀에 한 번으로 모은다** — 보이는 일뿐이다. */
|
|
310
|
-
let
|
|
311
|
-
let
|
|
312
|
-
const
|
|
313
|
-
const pt =
|
|
310
|
+
let rafId = null;
|
|
311
|
+
let lastPoint = null;
|
|
312
|
+
const computeIndex = () => {
|
|
313
|
+
const pt = lastPoint;
|
|
314
314
|
if (!pt)
|
|
315
315
|
return;
|
|
316
316
|
/* 손가락 밑의 칸. `elementFromPoint` 가 없는 환경(구형·시험)에서는 자리를 못 고른다 —
|
|
317
317
|
그때는 떨어뜨려도 아무 일이 없다(끌기가 시작되지 않은 것과 같다). */
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
const i =
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
setDragOver(prev => (prev ===
|
|
318
|
+
const under = document.elementFromPoint?.(pt.x, pt.y);
|
|
319
|
+
const cell = under?.closest?.('[data-wordtable-cell]');
|
|
320
|
+
const i = cell ? Number(cell.dataset.wordtableCell) : NaN;
|
|
321
|
+
const overIndex = Number.isInteger(i) ? i : null;
|
|
322
|
+
dropIndex.current = overIndex;
|
|
323
|
+
setDragOver(prev => (prev === overIndex ? prev : overIndex));
|
|
324
324
|
};
|
|
325
|
-
const
|
|
326
|
-
if (
|
|
327
|
-
if (Math.abs(ev.clientX -
|
|
328
|
-
|
|
325
|
+
const onMove = (ev) => {
|
|
326
|
+
if (!armed) {
|
|
327
|
+
if (Math.abs(ev.clientX - start.x) > MOVE_THRESHOLD_PX || Math.abs(ev.clientY - start.y) > MOVE_THRESHOLD_PX)
|
|
328
|
+
cleanup();
|
|
329
329
|
return;
|
|
330
330
|
}
|
|
331
|
-
|
|
331
|
+
lastPoint = { x: ev.clientX, y: ev.clientY };
|
|
332
332
|
if (typeof requestAnimationFrame === 'undefined') {
|
|
333
|
-
|
|
333
|
+
computeIndex();
|
|
334
334
|
return;
|
|
335
335
|
}
|
|
336
|
-
if (
|
|
336
|
+
if (rafId !== null)
|
|
337
337
|
return;
|
|
338
|
-
|
|
338
|
+
rafId = requestAnimationFrame(() => { rafId = null; computeIndex(); });
|
|
339
339
|
};
|
|
340
|
-
const
|
|
340
|
+
const onUp = () => {
|
|
341
341
|
/* **떼기 전에 밀린 셈을 치른다.** 자리셈을 틀로 모았으니, 움직이고 **같은 틀 안에** 떼면 틀이
|
|
342
342
|
오기 전에 여기 닿는다 — 그러면 `떨굴자리` 가 비어 **떨어뜨린 자리를 잃는다.** 옛 시험
|
|
343
343
|
(`wordTableReorder0916`)이 그것을 짚었다. 모으는 고침을 할 때는 **그 값을 읽는 쪽**도 봐야 한다. */
|
|
344
|
-
if (
|
|
345
|
-
cancelAnimationFrame(
|
|
346
|
-
|
|
347
|
-
|
|
344
|
+
if (rafId !== null) {
|
|
345
|
+
cancelAnimationFrame(rafId);
|
|
346
|
+
rafId = null;
|
|
347
|
+
computeIndex();
|
|
348
348
|
}
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
|
|
349
|
+
const dropAt = dropIndex.current;
|
|
350
|
+
const didDrag = armed;
|
|
351
|
+
cleanup();
|
|
352
|
+
if (didDrag && dropAt != null && dropAt !== index)
|
|
353
|
+
move(index, dropAt);
|
|
354
354
|
};
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
355
|
+
const timer = setTimeout(() => {
|
|
356
|
+
armed = true;
|
|
357
|
+
setDragging(index);
|
|
358
|
+
dropIndex.current = index;
|
|
359
359
|
setDragOver(index);
|
|
360
360
|
/* **안-모음: `preventDefault()` 는 못 미룬다** — 틀로 미루면 이벤트가 이미 지나가 막을 수 없다
|
|
361
361
|
(`passive: false` 로 다는 까닭이 그것이다). */
|
|
362
|
-
window.addEventListener('touchmove',
|
|
363
|
-
},
|
|
364
|
-
function
|
|
365
|
-
clearTimeout(
|
|
366
|
-
window.removeEventListener('pointermove',
|
|
367
|
-
window.removeEventListener('pointerup',
|
|
368
|
-
window.removeEventListener('pointercancel',
|
|
369
|
-
window.removeEventListener('touchmove',
|
|
370
|
-
|
|
371
|
-
if (
|
|
372
|
-
cancelAnimationFrame(
|
|
373
|
-
|
|
362
|
+
window.addEventListener('touchmove', blockScroll, { passive: false });
|
|
363
|
+
}, LONG_PRESS_MS);
|
|
364
|
+
function cleanup() {
|
|
365
|
+
clearTimeout(timer);
|
|
366
|
+
window.removeEventListener('pointermove', onMove);
|
|
367
|
+
window.removeEventListener('pointerup', onUp);
|
|
368
|
+
window.removeEventListener('pointercancel', onUp);
|
|
369
|
+
window.removeEventListener('touchmove', blockScroll);
|
|
370
|
+
armed = false;
|
|
371
|
+
if (rafId !== null) {
|
|
372
|
+
cancelAnimationFrame(rafId);
|
|
373
|
+
rafId = null;
|
|
374
374
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
375
|
+
dropIndex.current = null;
|
|
376
|
+
release.current = null;
|
|
377
|
+
setDragging(null);
|
|
378
378
|
setDragOver(null);
|
|
379
379
|
}
|
|
380
|
-
|
|
381
|
-
window.addEventListener('pointermove',
|
|
382
|
-
window.addEventListener('pointerup',
|
|
383
|
-
window.addEventListener('pointercancel',
|
|
384
|
-
}, [onReorder,
|
|
380
|
+
release.current = cleanup;
|
|
381
|
+
window.addEventListener('pointermove', onMove);
|
|
382
|
+
window.addEventListener('pointerup', onUp);
|
|
383
|
+
window.addEventListener('pointercancel', onUp);
|
|
384
|
+
}, [onReorder, move]);
|
|
385
385
|
const { width, height } = WORD_TABLE_PAPER[orientation];
|
|
386
386
|
/* 담는 틀의 높이 — 수나 `{ wide, narrow }` 를 주면 표가 그 틀을 스스로 두른다(`true` 는 예전대로 부모 몫) */
|
|
387
|
-
const
|
|
388
|
-
: (fitHeight && typeof fitHeight === 'object') ? (
|
|
387
|
+
const frameHeight = typeof fitHeight === 'number' ? fitHeight
|
|
388
|
+
: (fitHeight && typeof fitHeight === 'object') ? (isNarrow ? fitHeight.narrow : fitHeight.wide)
|
|
389
389
|
: null;
|
|
390
390
|
// 스케일러 — 용지(width 고정)를 컨테이너 폭에 맞춰 축소 + 가운데 정렬. fitHeight면 높이도 함께 맞춘다.
|
|
391
391
|
const { scalerRef, scale, offset } = useFitScale(width, fitHeight ? height : undefined);
|
|
@@ -393,10 +393,10 @@ export default function WordTable({ title, titleIcon, disc, className, orientati
|
|
|
393
393
|
const cellCount = Math.max(count ?? dataLen, 1);
|
|
394
394
|
const { cols, rows } = gridFor(orientation, cellCount);
|
|
395
395
|
const show = {
|
|
396
|
-
word: contentTypes.length === 0 || contentTypes.includes('
|
|
397
|
-
image: contentTypes.length === 0 || contentTypes.includes('
|
|
398
|
-
pron: contentTypes.length === 0 || contentTypes.includes('
|
|
399
|
-
colloq: contentTypes.length === 0 || contentTypes.includes('
|
|
396
|
+
word: contentTypes.length === 0 || contentTypes.includes('word'),
|
|
397
|
+
image: contentTypes.length === 0 || contentTypes.includes('image'),
|
|
398
|
+
pron: contentTypes.length === 0 || contentTypes.includes('standardPron'),
|
|
399
|
+
colloq: contentTypes.length === 0 || contentTypes.includes('colloquialPron'),
|
|
400
400
|
};
|
|
401
401
|
const hasPairs = isEdit
|
|
402
402
|
? (editCells ?? []).some(c => c !== null && c.length > 1)
|
|
@@ -405,16 +405,16 @@ export default function WordTable({ title, titleIcon, disc, className, orientati
|
|
|
405
405
|
그 값들도 설 자리가 없다 — 켜는 스위치를 하나로 둔다. */
|
|
406
406
|
/* 띠를 쓰기로 한 종이인가 — 한쪽을 꺼도 이 종이는 '띠를 쓰는 꼴'이다.
|
|
407
407
|
쪽 번호가 어디에 서는지를 이 값이 가른다(아래를 보라). */
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
const
|
|
411
|
-
const
|
|
408
|
+
const showBrand = Boolean(brand?.show);
|
|
409
|
+
const showTopBand = Boolean(brand?.show) && brand?.top !== false;
|
|
410
|
+
const showBottomBand = Boolean(brand?.show) && brand?.bottom !== false;
|
|
411
|
+
const copyright = brand?.copyright ?? '© IHAB, Inc. All rights reserved.';
|
|
412
412
|
const contentClass = cx('word-table-block__content', CUT_CLASS[cutLine], showOutline && 'word-table-block__content--outline', showDivLine && hasPairs && 'word-table-block__content--div-line',
|
|
413
413
|
/* 번호가 설 자리를 비운다 — 번호가 없으면 비우지 않는다(표가 공연히 낮아지지 않게) */
|
|
414
|
-
|
|
414
|
+
!showBrand && pageNumber && pageNumberPosition !== 'none' && 'word-table-block__content--page-number',
|
|
415
415
|
/* 띠가 설 자리를 위아래로 비운다 — 같은 까닭으로 띠가 없으면 안 비운다.
|
|
416
416
|
바닥 띠가 쪽 번호 자리를 겸하므로 둘이 겹쳐도 아래는 한 번만 비운다(CSS 가 덮어쓴다). */
|
|
417
|
-
|
|
417
|
+
showTopBand && 'word-table-block__content--brand-top', showBottomBand && 'word-table-block__content--brand-bottom');
|
|
418
418
|
// 빈칸(추가버튼)은 스케일 역보정으로 실제 크기 유지
|
|
419
419
|
const counterScale = { transform: `scale(${1 / scale})`, transformOrigin: 'center center' };
|
|
420
420
|
/* **채점 단추는 표와 함께 줄어들면 안 된다.**
|
|
@@ -423,7 +423,7 @@ export default function WordTable({ title, titleIcon, disc, className, orientati
|
|
|
423
423
|
다만 끝까지 되돌리면(1/scale) 단추 셋(40×3 + 사이 8 = 128)이 **칸보다 넓어져** 옆 칸을
|
|
424
424
|
침범한다 — 칸 폭을 천장으로 둔다. 이 견줌은 표 좌표에서 하므로 배율이 양쪽에서 약분된다.
|
|
425
425
|
넓은 화면(배율 1)에서는 1 이라 아무것도 달라지지 않는다. */
|
|
426
|
-
const
|
|
426
|
+
const markRowWidth = buttonSize * MARKS.length + 4 * (MARKS.length - 1);
|
|
427
427
|
/* **누를 수 없는 표에서는 되돌리지 않는다**(말귀 요청 ㊷, 2026-10-01).
|
|
428
428
|
위 되돌림은 '손가락으로 누를 수 있게' 하려고 있는 것이다. `onMarkClick` 이 없으면
|
|
429
429
|
눌러도 아무 일이 없으므로(바로 위 `clickMark` 가 빈 호출이다) 되돌릴 까닭이 없고,
|
|
@@ -431,19 +431,19 @@ export default function WordTable({ title, titleIcon, disc, className, orientati
|
|
|
431
431
|
배율을 그대로 받아 작아져, 칸이 작을수록 표시가 **낱말 위로 올라탄다**
|
|
432
432
|
(말귀 실측: 데스크톱 배율 0.54·40px 에서 `사과`·`수박` 을 덮었다).
|
|
433
433
|
지난 채점을 **매긴 그 표 그대로** 펴 보이는 창이 그 자리다. */
|
|
434
|
-
const
|
|
435
|
-
const
|
|
434
|
+
const canMark = Boolean(onMarkClick);
|
|
435
|
+
const markUnscale = canMark ? Math.max(1, Math.min(1 / scale, (width / cols) / markRowWidth)) : 1;
|
|
436
436
|
const grid = (_jsxs("div", { ref: scalerRef, className: "word-table__scaler", style: { height: height * scale }, children: [_jsx("div", {
|
|
437
437
|
/* title 로 Section 에 감쌀 때는 className 을 바깥 한 곳에만 — 두 곳에 붙어 소비자 스타일이 두 번 걸렸다 */
|
|
438
438
|
/* 읽기만 하는 채점 표 — 안 매긴 ○△✕ 를 드러내지 않는다(CSS 가 받는다).
|
|
439
439
|
드러내는 규칙은 '매길 수 있게' 하려고 있는 것이라, 손가락 기기에서 셋이
|
|
440
440
|
늘 서면 **읽기만 하는 표가 아직 안 매긴 표처럼** 보였다(말귀 요청 ㊷). */
|
|
441
|
-
className: cx('word-table', scoring &&
|
|
441
|
+
className: cx('word-table', scoring && !canMark && 'word-table--score-readonly', !wrapped && className), style: { width, height, transform: `scale(${scale})`, transformOrigin: 'top left', marginLeft: offset }, children: _jsxs("div", { ref: gridContentRef, className: contentClass, style: {
|
|
442
442
|
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
|
443
443
|
gridTemplateRows: `repeat(${rows}, 1fr)`,
|
|
444
444
|
}, children: [isEdit ? (_jsx(_Fragment, { children: (editCells ?? []).map((cell, i) => (_jsx("div", { className: cx('word-table-block__edit-cell', cell ? 'word-table-block__edit-cell--filled' : 'word-table-block__edit-cell--empty', activeCell === i ? 'word-table-block__edit-cell--active' : ''), style: dotDashStyle(i, cols, rows, cutLine), children: cell ? (_jsxs(_Fragment, { children: [cell.map((e, wi) => (_jsxs(Fragment, { children: [wi > 0 && _jsx("div", { className: "word-table-block__pair-divider" }), _jsx(WordCell, { label: e.word, subLabel: e.pronunciation,
|
|
445
445
|
// 편집 칸도 구어체 발음을 넘긴다 — 빠져 있어 '구어체 발음'을 켜도 편집 모드에서만 비었다.
|
|
446
|
-
colloquialLabel: e.colloquialPronunciation, homonum: e.homonum, showWord: show.word, showImage: show.image, showPronunciation: show.pron, showColloquial: show.colloq, buttonSize: buttonSize })] }, wi))), scoring && renderScore(i, cell.map(e => e.word).join(' / ')), onRemoveCell && (_jsx("div", { className: "word-table-block__card-close", children: _jsx(Button, { htmlType: "button", size: buttonSize, type: "dot", icon: "x", variant: "transparent-lightGray", useBorder: false, showTooltipInfo: cell.length > 1 ? t('wordPrint.removePair', '대립쌍 빼기') : t('wordPrint.removeWord', '단어 빼기'), ariaLabel: cell.length > 1 ? t('wordPrint.removePair', '대립쌍 빼기') : t('wordPrint.removeWord', '단어 빼기'), onClick: () => onRemoveCell(i) }) }))] })) : (onActivateCell && (_jsx("div", { style: counterScale, children: _jsx(Button, { htmlType: "button", size: "48", type: "bar", icon: "plus", label: t('wordTable.addWord', '추가하기'), onClick: () => onActivateCell(i) }) }))) }, i))) })) : (_jsxs(_Fragment, { children: [(items ?? []).map((item, index) => (_jsxs("div", { "data-wordtable-cell": onReorder ? index : undefined, className: `word-table-block__card${onReorder ? ' word-table-block__card--draggable' : ''}${dragOver === index ? ' word-table-block__card--drag-over' : ''}${
|
|
446
|
+
colloquialLabel: e.colloquialPronunciation, homonum: e.homonum, showWord: show.word, showImage: show.image, showPronunciation: show.pron, showColloquial: show.colloq, buttonSize: buttonSize })] }, wi))), scoring && renderScore(i, cell.map(e => e.word).join(' / ')), onRemoveCell && (_jsx("div", { className: "word-table-block__card-close", children: _jsx(Button, { htmlType: "button", size: buttonSize, type: "dot", icon: "x", variant: "transparent-lightGray", useBorder: false, showTooltipInfo: cell.length > 1 ? t('wordPrint.removePair', '대립쌍 빼기') : t('wordPrint.removeWord', '단어 빼기'), ariaLabel: cell.length > 1 ? t('wordPrint.removePair', '대립쌍 빼기') : t('wordPrint.removeWord', '단어 빼기'), onClick: () => onRemoveCell(i) }) }))] })) : (onActivateCell && (_jsx("div", { style: counterScale, children: _jsx(Button, { htmlType: "button", size: "48", type: "bar", icon: "plus", label: t('wordTable.addWord', '추가하기'), onClick: () => onActivateCell(i) }) }))) }, i))) })) : (_jsxs(_Fragment, { children: [(items ?? []).map((item, index) => (_jsxs("div", { "data-wordtable-cell": onReorder ? index : undefined, className: `word-table-block__card${onReorder ? ' word-table-block__card--draggable' : ''}${dragOver === index ? ' word-table-block__card--drag-over' : ''}${dragging === index ? ' word-table-block__card--dragging' : ''}`, style: dotDashStyle(index, cols, rows, cutLine), draggable: !!onReorder, onPointerDown: onReorder ? (e) => longPressDrag(e, index) : undefined, onKeyDown: onReorder ? (e) => moveByKey(e, index) : undefined, onDragStart: onReorder ? () => { dragIndex.current = index; } : undefined, onDragOver: onReorder ? (e) => { e.preventDefault(); setDragOver(index); } : undefined, onDragLeave: onReorder ? () => setDragOver(null) : undefined, onDrop: onReorder ? (e) => { e.preventDefault(); if (dragIndex.current !== null && dragIndex.current !== index) {
|
|
447
447
|
onReorder(dragIndex.current, index);
|
|
448
448
|
} ; dragIndex.current = null; setDragOver(null); } : undefined, onDragEnd: onReorder ? () => { dragIndex.current = null; setDragOver(null); } : undefined, children: [(item.words ?? []).map((w, i) => (_jsxs(Fragment, { children: [i > 0 && _jsx("div", { className: "word-table-block__pair-divider" }), _jsx(WordCell, { label: w.label, subLabel: w.subLabel, colloquialLabel: w.colloquialLabel, homonum: w.homonum, showWord: show.word, showImage: show.image, showPronunciation: show.pron, showColloquial: show.colloq, buttonSize: buttonSize, wordInfo: (w.grade || w.syllables != null || w.pos || w.wordType) ? { grade: w.grade, syllables: w.syllables, pos: w.pos, wordType: w.wordType } : undefined })] }, i))), scoring && renderScore(index, (item.words ?? []).map(w => w.label).join(' / ')), onReorder && (
|
|
449
449
|
/* **옮기기 손잡이.** 키보드로 순서를 바꾸려면 초점이 갈 자리가 있어야 하는데,
|
|
@@ -460,26 +460,26 @@ export default function WordTable({ title, titleIcon, disc, className, orientati
|
|
|
460
460
|
_jsxs("div", { className: "word-table-block__card-actions", children: [renderCellAction?.(index, item), onRemoveItem && (_jsx("div", { className: "word-table-block__card-close", children: _jsx(Button, { htmlType: "button", size: buttonSize, type: "dot", icon: "x", variant: "transparent-lightGray", useBorder: false, showTooltipInfo: (item.words ?? []).length > 1 ? t('wordPrint.removePair', '대립쌍 빼기') : t('wordPrint.removeWord', '단어 빼기'), ariaLabel: (item.words ?? []).length > 1 ? t('wordPrint.removePair', '대립쌍 빼기') : t('wordPrint.removeWord', '단어 빼기'), onClick: () => onRemoveItem(item.id) }) }))] }))] }, item.id))), Array.from({ length: Math.max(0, cols * rows - (items?.length ?? 0)) }).map((_, i) => {
|
|
461
461
|
const isWithinCount = (items?.length ?? 0) + i < cellCount;
|
|
462
462
|
return (_jsx("div", { className: "word-table-block__card word-table-block__card--filler", children: showFillerAdd && onActivateCell && isWithinCount && (_jsx("div", { style: counterScale, children: _jsx(Button, { htmlType: "button", size: buttonSize, type: "bar", icon: "plus", label: t('wordTable.addWord', '추가하기'), onClick: () => onActivateCell((items?.length ?? 0) + i) }) })) }, `filler-${i}`));
|
|
463
|
-
})] })),
|
|
463
|
+
})] })), showTopBand && (_jsx(_Fragment, { children: _jsxs("div", { className: "word-table__band word-table__band--top", children: [_jsx("img", { className: "word-table__brand-logo", src: brand?.topLogo?.src ?? speechearLogo, alt: brand?.topLogo?.alt ?? '말귀' }), (() => {
|
|
464
464
|
/* 표 제목 · 기관 이름 · 표 번호 — 있는 것끼리만 가운뎃점으로 잇는다
|
|
465
465
|
(사용자 지시 2026-09-30). 없는 것 자리에 점이 남으면 안 된다. */
|
|
466
|
-
const
|
|
466
|
+
const pieces = [
|
|
467
467
|
brand?.title && _jsx("span", { className: "word-table__paper-title", children: brand.title }, "t"),
|
|
468
468
|
brand?.owner && _jsx("span", { className: "word-table__owner", children: brand.owner }, "o"),
|
|
469
469
|
code && _jsx("span", { className: "word-table__code", children: code }, "c"),
|
|
470
470
|
].filter(Boolean);
|
|
471
|
-
if (
|
|
471
|
+
if (pieces.length === 0)
|
|
472
472
|
return null;
|
|
473
|
-
return (_jsx("span", { className: "word-table__ident", children:
|
|
474
|
-
})()] }) })),
|
|
473
|
+
return (_jsx("span", { className: "word-table__ident", children: pieces.map((piece, i) => (_jsxs(Fragment, { children: [i > 0 && _jsx("span", { className: "word-table__ident-dot", "aria-hidden": "true", children: "\u00B7" }), piece] }, i))) }));
|
|
474
|
+
})()] }) })), showBottomBand && (_jsx(_Fragment, { children: _jsxs("div", { className: "word-table__band word-table__band--bottom", children: [_jsxs("span", { className: "word-table__brand-by", children: [brand?.bottomLogo
|
|
475
475
|
? _jsx("img", { className: "word-table__brand-logo--ihab word-table__brand-logo--given", src: brand.bottomLogo.src, alt: brand.bottomLogo.alt })
|
|
476
|
-
: _jsx("span", { className: "word-table__brand-logo--ihab", role: "img", "aria-label": "IHAB" }), _jsx("span", { className: "word-table__copyright", children:
|
|
476
|
+
: _jsx("span", { className: "word-table__brand-logo--ihab", role: "img", "aria-label": "IHAB" }), _jsx("span", { className: "word-table__copyright", children: copyright })] }), _jsxs("span", { className: "word-table__band-end", children: [brand?.link && _jsx("span", { className: "word-table__link", children: brand.link }), pageNumber && pageNumberPosition !== 'none' && (_jsx("span", { className: "word-table__page-count", children: t('wordTable.filter.pageIndicator', { current: pageNumber.current, total: pageNumber.total }) }))] })] }) })), !showBrand && pageNumber && pageNumberPosition !== 'none' && (_jsx("span", { className: `word-table__page-number body-3 word-table__page-number--${pageNumberPosition}`, children: t('wordTable.filter.pageIndicator', { current: pageNumber.current, total: pageNumber.total }) }))] }) }), onReorder && _jsx("span", { role: "status", style: srOnly, children: moveAnnounce })] }));
|
|
477
477
|
/* 담는 틀 — 높이를 받은 자리에서만 두른다. `useFitScale` 은 **스케일러의 부모**를 재므로
|
|
478
478
|
이 틀이 곧 그 자(척도)가 된다. 가운데 맞춤은 남는 세로를 위아래로 나눈다 — 표가 상자보다
|
|
479
479
|
납작할 때 아래로만 쏠려 보이지 않게. */
|
|
480
|
-
const
|
|
480
|
+
const contents = frameHeight == null ? grid : (_jsx("div", { className: "word-table__frame", style: { height: frameHeight }, children: grid }));
|
|
481
481
|
if (wrapped) {
|
|
482
|
-
return (_jsx(Section, { type: "page", icon: titleIcon, title: title, disc: disc, className: cx('word-table-block', className), children:
|
|
482
|
+
return (_jsx(Section, { type: "page", icon: titleIcon, title: title, disc: disc, className: cx('word-table-block', className), children: contents }));
|
|
483
483
|
}
|
|
484
|
-
return
|
|
484
|
+
return contents;
|
|
485
485
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TFunction } from 'i18next';
|
|
1
2
|
/** 막대 하나의 데이터 — a/b 두 계열. a === -1 이면 미래(데이터 없음)로 표시하지 않음.
|
|
2
3
|
* parts: 다계열용 계열별 값(있으면 a는 합계로 사용).
|
|
3
4
|
* partsB: `parts` 와 1:1 인 계열별 **분모** — `rate` 에서 계열마다 parts[k]/partsB[k] 를 낸다(a/b 의 계열판). */
|
|
@@ -67,4 +68,18 @@ export declare function monthOptionsNow(): {
|
|
|
67
68
|
year: number;
|
|
68
69
|
month: number;
|
|
69
70
|
}[];
|
|
70
|
-
|
|
71
|
+
/**
|
|
72
|
+
* 몇째 주인가를 **지금 언어로** 적는다 — `0` 이 첫째 주다.
|
|
73
|
+
*
|
|
74
|
+
* **언어 가름을 부르는 쪽이 하지 않는다.** 전에는 한글 서수 배열(`KO_ORD`)을 내주고
|
|
75
|
+
* 쓰는 쪽마다 `lang.startsWith('ko') ? KO_ORD[w] + '주' : 'Week ' + (w+1)` 를 되풀이했다 —
|
|
76
|
+
* 코어 두 자리와 말귀 한 자리가 **같은 가름을 각자** 적고 있었다. 말이 하나 늘면 세 자리를
|
|
77
|
+
* 다 고쳐야 하고, 한 자리만 안 고치면 화면에서 갈린다.
|
|
78
|
+
*
|
|
79
|
+
* 글귀는 사전이 쥔다(`calendar.weekLabel`·`calendar.weekOrdinal`) — 한국어는 서수가
|
|
80
|
+
* 규칙으로 안 나와(첫째·둘째…) 표가 필요하고, **그 표의 자리는 코드가 아니라 사전이다.**
|
|
81
|
+
* 서수 표가 없는 말은 `{{n}}` 으로 적는다(영어 `Week 3`).
|
|
82
|
+
*
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
85
|
+
export declare function weekLabel(t: TFunction, week: number): string;
|