@ihabdevteam/core 0.150.0 → 0.152.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 +71 -0
- package/dist/components.cjs.js +199 -186
- package/dist/components.esm.css +5 -5
- package/dist/hooks.cjs.js +5 -1
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core-tokens.css +1 -1
- package/dist/ihabdevteam-core.cjs.js +200 -187
- package/dist/ihabdevteam-core.css +1 -1
- package/dist/ihabdevteam-core.d.ts +405 -67
- package/dist/packages/core/src/components/Accordion.d.ts +6 -0
- package/dist/packages/core/src/components/Accordion.js +5 -2
- package/dist/packages/core/src/components/Alert.d.ts +7 -1
- package/dist/packages/core/src/components/Alert.js +12 -3
- package/dist/packages/core/src/components/BarChart.js +7 -2
- package/dist/packages/core/src/components/Board.d.ts +5 -2
- package/dist/packages/core/src/components/Board.js +9 -3
- package/dist/packages/core/src/components/BottomNav.js +5 -4
- package/dist/packages/core/src/components/Breadcrumb.css +9 -0
- package/dist/packages/core/src/components/Breadcrumb.d.ts +12 -1
- package/dist/packages/core/src/components/Breadcrumb.js +8 -2
- package/dist/packages/core/src/components/BulkBar.css +2 -0
- package/dist/packages/core/src/components/BulkBar.js +21 -2
- package/dist/packages/core/src/components/Button.d.ts +12 -2
- package/dist/packages/core/src/components/Button.js +70 -13
- package/dist/packages/core/src/components/Calendar.d.ts +6 -1
- package/dist/packages/core/src/components/Calendar.js +3 -3
- package/dist/packages/core/src/components/ChartSection.css +11 -4
- package/dist/packages/core/src/components/DatePicker.d.ts +7 -1
- package/dist/packages/core/src/components/DatePicker.js +25 -15
- package/dist/packages/core/src/components/Dropdown.d.ts +1 -0
- package/dist/packages/core/src/components/Dropdown.js +7 -5
- package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.d.ts +12 -1
- package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.js +25 -8
- package/dist/packages/core/src/components/EmptyState.d.ts +5 -1
- package/dist/packages/core/src/components/EmptyState.js +4 -1
- package/dist/packages/core/src/components/ErrorState.d.ts +5 -2
- package/dist/packages/core/src/components/ErrorState.js +3 -1
- package/dist/packages/core/src/components/FileUpload.d.ts +9 -1
- package/dist/packages/core/src/components/FileUpload.js +8 -2
- package/dist/packages/core/src/components/FilterSectionInfo.js +14 -2
- package/dist/packages/core/src/components/Header.js +8 -5
- package/dist/packages/core/src/components/HearingDeviceInfo.js +2 -1
- package/dist/packages/core/src/components/Icon.d.ts +2 -1
- package/dist/packages/core/src/components/Icon.js +4 -1
- package/dist/packages/core/src/components/Loader.css +28 -0
- package/dist/packages/core/src/components/Loader.d.ts +5 -1
- package/dist/packages/core/src/components/Loader.js +4 -2
- package/dist/packages/core/src/components/NoticeBox.d.ts +1 -1
- package/dist/packages/core/src/components/NoticeBox.js +15 -6
- package/dist/packages/core/src/components/PanelRow.js +2 -1
- package/dist/packages/core/src/components/PassCard/PassCard.d.ts +8 -2
- package/dist/packages/core/src/components/PassCard/PassCard.js +25 -10
- package/dist/packages/core/src/components/PassCard/passCardProduct.d.ts +1 -1
- package/dist/packages/core/src/components/PassCard/passCardProduct.js +11 -2
- package/dist/packages/core/src/components/Popup/ConfirmPopup.d.ts +11 -2
- package/dist/packages/core/src/components/Popup/ConfirmPopup.js +18 -3
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +10 -8
- package/dist/packages/core/src/components/Popup/ListPopup.js +6 -6
- package/dist/packages/core/src/components/Popup/LoadTablePopup.css +7 -0
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +27 -5
- package/dist/packages/core/src/components/Popup/Popup.d.ts +8 -0
- package/dist/packages/core/src/components/Popup/Popup.js +6 -3
- package/dist/packages/core/src/components/Popup/SaveTablePopup.d.ts +5 -1
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.css +3 -0
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.d.ts +5 -0
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +10 -4
- package/dist/packages/core/src/components/Popup/WordDataUpdatePopup.css +9 -0
- package/dist/packages/core/src/components/ProgressIndicator.css +5 -0
- package/dist/packages/core/src/components/ProgressIndicator.d.ts +3 -1
- package/dist/packages/core/src/components/ProgressIndicator.js +5 -3
- package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.d.ts +4 -1
- package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +27 -15
- package/dist/packages/core/src/components/Rating.d.ts +8 -2
- package/dist/packages/core/src/components/Rating.js +22 -8
- package/dist/packages/core/src/components/Report/ReportDocument.d.ts +10 -7
- package/dist/packages/core/src/components/Report/ReportDocument.js +39 -13
- package/dist/packages/core/src/components/RingChart.d.ts +1 -1
- package/dist/packages/core/src/components/RingChart.js +1 -1
- package/dist/packages/core/src/components/SegmentedControl.d.ts +4 -0
- package/dist/packages/core/src/components/SegmentedControl.js +9 -1
- package/dist/packages/core/src/components/ShortcutBadge.css +16 -1
- package/dist/packages/core/src/components/ShortcutBadge.d.ts +1 -0
- package/dist/packages/core/src/components/SideNav.js +8 -2
- package/dist/packages/core/src/components/SummaryStrip.d.ts +1 -1
- package/dist/packages/core/src/components/SummaryStrip.js +2 -2
- package/dist/packages/core/src/components/Table.d.ts +1 -1
- package/dist/packages/core/src/components/Table.js +4 -1
- package/dist/packages/core/src/components/Tabs.d.ts +1 -0
- package/dist/packages/core/src/components/Tabs.js +4 -1
- package/dist/packages/core/src/components/TextField.css +5 -6
- package/dist/packages/core/src/components/TextField.js +46 -3
- package/dist/packages/core/src/components/Toast.js +16 -2
- package/dist/packages/core/src/components/Tooltip/Tooltip.css +3 -1
- package/dist/packages/core/src/components/Tooltip/Tooltip.js +5 -2
- package/dist/packages/core/src/components/Tooltip/tooltipConstants.d.ts +10 -2
- package/dist/packages/core/src/components/Tooltip/tooltipConstants.js +10 -2
- package/dist/packages/core/src/components/TopBanner.d.ts +1 -1
- package/dist/packages/core/src/components/TopBanner.js +66 -8
- package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.d.ts +1 -0
- package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +65 -15
- package/dist/packages/core/src/components/Training/NoisePlayerContext.d.ts +3 -2
- package/dist/packages/core/src/components/Training/NoisePlayerContext.js +10 -4
- package/dist/packages/core/src/components/Training/TrainingBlockButton.css +7 -0
- package/dist/packages/core/src/components/Training/TrainingBlockButton.js +3 -1
- package/dist/packages/core/src/components/Training/TrainingPagePopup.d.ts +18 -5
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +93 -28
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.d.ts +1 -0
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +16 -3
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +25 -9
- package/dist/packages/core/src/components/Training/TrainingStepCard.js +19 -3
- package/dist/packages/core/src/components/Training/TrainingStepTitle.js +15 -1
- package/dist/packages/core/src/components/Training/TrainingWordCard.css +16 -0
- package/dist/packages/core/src/components/Training/TrainingWordCard.d.ts +6 -0
- package/dist/packages/core/src/components/Training/TrainingWordCard.js +7 -2
- package/dist/packages/core/src/components/Training/trainingBlockButtonTypes.d.ts +2 -2
- package/dist/packages/core/src/components/Training/trainingMissStats.d.ts +10 -0
- package/dist/packages/core/src/components/Training/trainingMissStats.js +19 -1
- package/dist/packages/core/src/components/Training/useFloatingFocus.d.ts +13 -0
- package/dist/packages/core/src/components/Training/useFloatingFocus.js +59 -0
- package/dist/packages/core/src/components/Training/useNoiseFade.js +20 -11
- package/dist/packages/core/src/components/Training/useProgressTicker.d.ts +0 -18
- package/dist/packages/core/src/components/Training/useProgressTicker.js +59 -14
- package/dist/packages/core/src/components/Training/useTTSPlayback.d.ts +1 -1
- package/dist/packages/core/src/components/Training/useTTSPlayback.js +56 -25
- package/dist/packages/core/src/components/Training/useWaveReady.d.ts +4 -1
- package/dist/packages/core/src/components/Training/useWaveReady.js +17 -8
- package/dist/packages/core/src/components/UpdateAlert.css +4 -2
- package/dist/packages/core/src/components/UpdateAlert.d.ts +3 -1
- package/dist/packages/core/src/components/UpdateAlert.js +21 -1
- package/dist/packages/core/src/components/Word/PhonemeSelectBlock.d.ts +2 -0
- package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +12 -4
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.d.ts +6 -0
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +26 -13
- package/dist/packages/core/src/components/Word/WordTableBlock.js +24 -21
- package/dist/packages/core/src/components/Word/WordTableThumbnail.js +1 -1
- package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -10
- package/dist/packages/core/src/components/Word/useWordTablePrint.d.ts +1 -1
- package/dist/packages/core/src/components/Word/useWordTablePrint.js +9 -3
- package/dist/packages/core/src/components/Word/wordScoreStats.d.ts +14 -1
- package/dist/packages/core/src/components/Word/wordScoreStats.js +7 -3
- package/dist/packages/core/src/components/Word/wordTablePrint.js +15 -3
- package/dist/packages/core/src/components/Word/wordTableReview.d.ts +58 -1
- package/dist/packages/core/src/components/Word/wordTableReview.js +75 -17
- package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +9 -0
- package/dist/packages/core/src/components/Word/wordTableSettings.js +11 -0
- package/dist/packages/core/src/components/WordTable.css +10 -0
- package/dist/packages/core/src/components/WordTable.js +39 -26
- package/dist/packages/core/src/components/barChartTypes.d.ts +2 -2
- package/dist/packages/core/src/components/buttonTypes.d.ts +8 -0
- package/dist/packages/core/src/components/updateAlertStack.d.ts +2 -0
- package/dist/packages/core/src/components/updateAlertStack.js +68 -0
- package/dist/packages/core/src/components/useCalendarMonth.d.ts +4 -1
- package/dist/packages/core/src/components/useCalendarMonth.js +6 -3
- package/dist/packages/core/src/components/useComboboxSuggestions.d.ts +7 -0
- package/dist/packages/core/src/components/useComboboxSuggestions.js +21 -2
- package/dist/packages/core/src/components/wordTableLayout.d.ts +4 -0
- package/dist/packages/core/src/components/wordTableLayout.js +6 -0
- package/dist/packages/core/src/components.d.ts +2 -1
- package/dist/packages/core/src/components.js +1 -1
- package/dist/packages/core/src/hooks/bodyScrollLock.js +8 -2
- package/dist/packages/core/src/hooks/focusReturn.d.ts +12 -0
- package/dist/packages/core/src/hooks/focusReturn.js +49 -0
- package/dist/packages/core/src/hooks/touchToggle.d.ts +22 -0
- package/dist/packages/core/src/hooks/touchToggle.js +114 -0
- package/dist/packages/core/src/hooks/useAnchoredMenu.d.ts +12 -3
- package/dist/packages/core/src/hooks/useAnchoredMenu.js +37 -3
- package/dist/packages/core/src/hooks/useDailyTrainingSelection.d.ts +4 -2
- package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +23 -11
- package/dist/packages/core/src/hooks/useIcon.js +29 -5
- package/dist/packages/core/src/hooks/useLucideIcon.d.ts +4 -0
- package/dist/packages/core/src/hooks/useLucideIcon.js +4 -0
- package/dist/packages/core/src/hooks/usePendingPress.d.ts +9 -0
- package/dist/packages/core/src/hooks/usePendingPress.js +26 -0
- package/dist/packages/core/src/hooks/usePrefersReducedMotion.d.ts +15 -0
- package/dist/packages/core/src/hooks/usePrefersReducedMotion.js +26 -0
- package/dist/packages/core/src/hooks/useTablePaging.d.ts +27 -2
- package/dist/packages/core/src/hooks/useTablePaging.js +37 -5
- package/dist/packages/core/src/hooks/useWordAttributeFilter.d.ts +7 -0
- package/dist/packages/core/src/hooks/useWordAttributeFilter.js +25 -0
- package/dist/packages/core/src/i18n/index.js +9 -1
- package/dist/packages/core/src/i18n/locales/en.json +24 -2
- package/dist/packages/core/src/i18n/locales/ko.json +24 -2
- package/dist/packages/core/src/tokens.css +5 -0
- package/dist/packages/core/src/types/wordSearch.d.ts +12 -0
- package/dist/packages/core/src/utils/csv.d.ts +7 -2
- package/dist/packages/core/src/utils/csv.js +12 -5
- package/dist/packages/core/src/utils/dailySelectionUtils.js +44 -14
- package/dist/packages/core/src/utils/date.d.ts +8 -0
- package/dist/packages/core/src/utils/date.js +44 -10
- package/dist/packages/core/src/utils/format.d.ts +1 -1
- package/dist/packages/core/src/utils/format.js +6 -7
- package/dist/packages/core/src/utils/noiseSettingsStore.d.ts +7 -3
- package/dist/packages/core/src/utils/phonemeConvert.d.ts +9 -0
- package/dist/packages/core/src/utils/phonemeConvert.js +11 -0
- package/dist/packages/core/src/utils/phonemePairSearch.d.ts +7 -1
- package/dist/packages/core/src/utils/phonemePairSearch.js +45 -21
- package/dist/packages/core/src/utils/safeHref.d.ts +8 -0
- package/dist/packages/core/src/utils/safeHref.js +24 -0
- package/dist/packages/core/src/utils/savedWordTables.d.ts +19 -3
- package/dist/packages/core/src/utils/savedWordTables.js +27 -19
- package/dist/packages/core/src/utils/textSort.d.ts +9 -0
- package/dist/packages/core/src/utils/textSort.js +37 -1
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +31 -0
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +163 -0
- package/dist/packages/core/src/utils/wordContrast.d.ts +4 -2
- package/dist/packages/core/src/utils/wordContrast.js +8 -2
- package/dist/packages/core/src/utils/wordTrainingGenerators.d.ts +5 -2
- package/dist/packages/core/src/utils/wordTrainingGenerators.js +52 -2
- package/dist/packages/core/src/utils/wordTrainingPool.js +2 -2
- package/dist/packages/core/src/utils/wordTrainingRuntime.js +15 -7
- package/dist/packages/core/src/utils/wordTrainingRuntimeTypes.d.ts +1 -1
- package/dist/packages/core/src/utils/wordTrainingRuntimeTypes.js +5 -4
- package/dist/packages/core/src/utils/wornDate.d.ts +12 -0
- package/dist/packages/core/src/utils/wornDate.js +15 -0
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +2 -2
- package/package.json +1 -1
|
@@ -22,6 +22,7 @@ import PassCardBgSoundMove from './PassCardBgSoundMove';
|
|
|
22
22
|
import PassCardBgSoundMoveV1 from './PassCardBgSoundMoveV1';
|
|
23
23
|
import PassCardBgTalk from './PassCardBgTalk';
|
|
24
24
|
import { getPassCardBgTheme } from './PassCardBgTheme';
|
|
25
|
+
import { recentlyTouched } from '../../hooks/touchToggle';
|
|
25
26
|
/*
|
|
26
27
|
* 로고 variant 정적 매핑 — base + color suffix 조합으로 SVG URL을 resolve.
|
|
27
28
|
* (Vite 전용 import.meta.glob 대신 정적 import — Rollup CJS 빌드 호환)
|
|
@@ -104,24 +105,26 @@ function getDefaultBg(type, plan, label) {
|
|
|
104
105
|
return _jsx(PassCardBgTwoCircle, {});
|
|
105
106
|
}
|
|
106
107
|
/** @public */
|
|
107
|
-
export default function PassCard({ product, showPrice = false, plan, type, endDate, remainingSeconds, state = 'active', bg, bgOnly = false, textColor, subColor, copyrightColor, logoSrc, label, price, originalPrice, discountLabel, onUse, onRefund, onDelete }) {
|
|
108
|
+
export default function PassCard({ product, showPrice = false, plan, type, endDate, remainingSeconds, endInclusive = false, state = 'active', bg, bgOnly = false, textColor, subColor, copyrightColor, logoSrc, label, price, originalPrice, discountLabel, onUse, onRefund, onDelete }) {
|
|
108
109
|
const { t } = useTranslation();
|
|
109
110
|
// product가 있으면 카드가 표시 속성을 스스로 도출. 명시적 prop이 항상 우선.
|
|
110
111
|
const d = product ? derivePassCard(product) : undefined;
|
|
111
112
|
const rType = type ?? d?.type ?? 'speechear';
|
|
112
|
-
const rPlan = plan ?? d?.plan ?? '이용권';
|
|
113
|
+
const rPlan = plan ?? d?.plan ?? t('passCard.planSuffix', '이용권');
|
|
113
114
|
const rLabel = label ?? d?.label;
|
|
114
115
|
const rPrice = price ?? (showPrice ? d?.price : undefined);
|
|
115
116
|
const rOriginalPrice = originalPrice ?? (showPrice ? d?.originalPrice : undefined);
|
|
116
117
|
const rDiscountLabel = discountLabel ?? d?.discountLabel;
|
|
117
118
|
// 사용량%·남은 시간 — rPrice가 있으면(가격 표시 카드) 그쪽이 우선이라 여긴 안 씀.
|
|
118
|
-
// 전체 기간을 모르면(product 미지정 등)
|
|
119
|
+
// 전체 기간을 모르면(product 미지정 등) %는 셀 수 없다 — remainingSeconds 를 받았으면 남은 시간만 보이고(전에는 product 가
|
|
120
|
+
// 없으면 remainingSeconds 를 줘도 통째로 버렸다), 그것도 없으면 표시할 게 없으므로 null.
|
|
119
121
|
const usageInfo = useMemo(() => {
|
|
120
122
|
if (rPrice)
|
|
121
123
|
return null;
|
|
122
124
|
const totalDays = product?.durationDays ?? deriveDurationDays(product?.code);
|
|
123
|
-
if (!totalDays || totalDays <= 0)
|
|
124
|
-
return null;
|
|
125
|
+
if (!totalDays || totalDays <= 0) {
|
|
126
|
+
return remainingSeconds != null ? { percent: null, remainingLabel: formatRemaining(remainingSeconds, t) } : null;
|
|
127
|
+
}
|
|
125
128
|
const totalSeconds = totalDays * 86400;
|
|
126
129
|
let remaining;
|
|
127
130
|
if (remainingSeconds != null) {
|
|
@@ -143,7 +146,8 @@ export default function PassCard({ product, showPrice = false, plan, type, endDa
|
|
|
143
146
|
const days = 끝 && 오늘
|
|
144
147
|
? Math.round((Date.UTC(+끝[1], +끝[2] - 1, +끝[3]) - Date.UTC(+오늘[1], +오늘[2] - 1, +오늘[3])) / 86_400_000)
|
|
145
148
|
: Math.round((end - Date.now()) / 86_400_000);
|
|
146
|
-
|
|
149
|
+
/* endInclusive — 종료일 당일까지 쓰는 이용권이면 그 날도 남은 날이다(마지막 날이 0초·0% 로 보이지 않게) */
|
|
150
|
+
remaining = Math.max(0, endInclusive ? days + 1 : days) * 86400;
|
|
147
151
|
}
|
|
148
152
|
else if (state === 'expired') {
|
|
149
153
|
// 만료된 카드인데 기준 날짜 정보가 전혀 없음 — 다 쓴 것으로 간주
|
|
@@ -161,7 +165,7 @@ export default function PassCard({ product, showPrice = false, plan, type, endDa
|
|
|
161
165
|
if (state === 'pending' && percent >= 100)
|
|
162
166
|
return null;
|
|
163
167
|
return { percent, remainingLabel: formatRemaining(remaining, t) };
|
|
164
|
-
}, [rPrice, state, endDate, remainingSeconds, product?.durationDays, product?.code, t]);
|
|
168
|
+
}, [rPrice, state, endDate, remainingSeconds, endInclusive, product?.durationDays, product?.code, t]);
|
|
165
169
|
const cardRef = useRef(null);
|
|
166
170
|
const [cardStyle, setCardStyle] = useState({});
|
|
167
171
|
const [menuAnchor, setMenuAnchor] = useState(null);
|
|
@@ -223,6 +227,9 @@ export default function PassCard({ product, showPrice = false, plan, type, endDa
|
|
|
223
227
|
return `${sx}px ${sy}px ${blur}px rgba(0, 0, 0, 0.18)`;
|
|
224
228
|
};
|
|
225
229
|
const handleMouseEnter = useCallback((e) => {
|
|
230
|
+
// 톡 뒤 따라오는 호환 마우스 이벤트로는 기울이지 않는다 — mouseleave 가 안 와 기울어진 채 남았다
|
|
231
|
+
if (recentlyTouched())
|
|
232
|
+
return;
|
|
226
233
|
isEnteringRef.current = true;
|
|
227
234
|
clearTimeout(enterTimerRef.current);
|
|
228
235
|
enterTimerRef.current = setTimeout(() => { isEnteringRef.current = false; }, 200);
|
|
@@ -236,6 +243,8 @@ export default function PassCard({ product, showPrice = false, plan, type, endDa
|
|
|
236
243
|
});
|
|
237
244
|
}, []);
|
|
238
245
|
const handleMouseMove = useCallback((e) => {
|
|
246
|
+
if (recentlyTouched())
|
|
247
|
+
return;
|
|
239
248
|
const tilt = calcTilt(e);
|
|
240
249
|
if (!tilt)
|
|
241
250
|
return;
|
|
@@ -257,7 +266,9 @@ export default function PassCard({ product, showPrice = false, plan, type, endDa
|
|
|
257
266
|
}, []);
|
|
258
267
|
const handleCloseMenu = useCallback(() => setMenuAnchor(null), []);
|
|
259
268
|
const planDisplay = useMemo(() => {
|
|
260
|
-
|
|
269
|
+
/* 폴백 플랜('이용권'·영어 'Plan')은 큰 글씨에서 뺀다 — 아래 꼬리(planSuffix)에 이미 적힌다 */
|
|
270
|
+
const 꼬리 = t('passCard.planSuffix', '이용권');
|
|
271
|
+
const text = rPlan.replace('이용권', '').replace(꼬리 === '이용권' ? '' : 꼬리, '').trim();
|
|
261
272
|
const match = text.match(/^(.*?)(일|년)$/);
|
|
262
273
|
return match
|
|
263
274
|
? _jsxs(_Fragment, { children: [_jsx("span", { className: "h0 pass-card__plan-number", children: match[1] }), _jsx("span", { className: "h5b pass-card__plan-unit", children: match[2] === '년' ? t('passCard.unitYear', '년') : t('passCard.unitDay', '일') })] })
|
|
@@ -307,11 +318,15 @@ export default function PassCard({ product, showPrice = false, plan, type, endDa
|
|
|
307
318
|
...(onDelete ? [{ label: t('passCard.menu.delete', '삭제하기'), icon: 'trash-2', onClick: () => { onDelete(); setMenuAnchor(null); }, closeOnSelect: true }] : []),
|
|
308
319
|
],
|
|
309
320
|
}];
|
|
310
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "pass-card-frame", children: _jsxs("div", { ref: cardRef, className: `pass-card pass-card--${state}${hasMenu ? ' pass-card--clickable' : ''}`, style: cardStyle, onMouseEnter: handleMouseEnter, onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave,
|
|
321
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "pass-card-frame", children: _jsxs("div", { ref: cardRef, className: `pass-card pass-card--${state}${hasMenu ? ' pass-card--clickable' : ''}`, style: cardStyle, onMouseEnter: handleMouseEnter, onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave,
|
|
322
|
+
/* 터치를 떼거나 취소되면 되돌린다 — 톡 뒤 기울기가 남았다(터치에는 mouseleave 가 안 온다) */
|
|
323
|
+
onPointerUp: (e) => { if (e.pointerType === 'touch')
|
|
324
|
+
handleMouseLeave(); }, onPointerCancel: (e) => { if (e.pointerType === 'touch')
|
|
325
|
+
handleMouseLeave(); }, onClick: handleCardClick, onKeyDown: hasMenu ? handleCardKeyDown : undefined, role: hasMenu ? 'button' : undefined, tabIndex: hasMenu ? 0 : undefined, "aria-haspopup": hasMenu ? 'menu' : undefined, "aria-expanded": hasMenu ? !!menuAnchor : undefined, children: [bg ?? getDefaultBg(rType, rPlan, rLabel), !bgOnly && (_jsxs(_Fragment, { children: [_jsx("div", { className: "pass-card__top", children: _jsxs("div", { className: "pass-card__top-left", children: [_jsx("img", { src: resolvedLogoSrc ?? LOGO[rType], className: "pass-card__logo", alt: rType === 'manager'
|
|
311
326
|
? t('header.logoManager', '말귀매니저 로고')
|
|
312
327
|
: t('header.logoMalgwi', '말귀 로고') }), _jsxs("div", { className: "pass-card__plan", children: [_jsx("div", { children: planDisplay }), _jsx("span", { className: "h5b pass-card__plan-suffix", children: rLabel ? t('passCard.labeledPlan', { label: rLabel }) : t('passCard.planSuffix', '이용권') })] })] }) }), rPrice
|
|
313
328
|
? (_jsxs("div", { className: "pass-card__price-area", children: [rDiscountLabel && _jsx("span", { className: "pass-card__discount body-3", children: rDiscountLabel }), _jsxs("div", { className: "pass-card__price", children: [rOriginalPrice && _jsx("span", { className: "pass-card__price-original body-3", children: toWonSymbol(rOriginalPrice) }), priceDisplay] })] }))
|
|
314
329
|
: usageInfo
|
|
315
|
-
? (_jsxs("div", { className: "pass-card__usage", children: [_jsx("span", { className: "h5b pass-card__usage-remaining", children: usageInfo.remainingLabel }), _jsxs("span", { className: "pass-card__usage-percent", children: [_jsx("span", { className: "h1 pass-card__usage-value", children: usageInfo.percent }), _jsx("span", { className: "h5b pass-card__usage-symbol", children: "%" })] })] }))
|
|
330
|
+
? (_jsxs("div", { className: "pass-card__usage", children: [_jsx("span", { className: "h5b pass-card__usage-remaining", children: usageInfo.remainingLabel }), usageInfo.percent != null && (_jsxs("span", { className: "pass-card__usage-percent", children: [_jsx("span", { className: "h1 pass-card__usage-value", children: usageInfo.percent }), _jsx("span", { className: "h5b pass-card__usage-symbol", children: "%" })] }))] }))
|
|
316
331
|
: _jsx("div", { className: "pass-card__copyright caption-2", children: "\u00A9 IHAB Corp. All rights reserved." })] }))] }) }), menuAnchor && (_jsx(Tooltip, { anchorRect: menuAnchor, onClose: handleCloseMenu, ariaLabel: t('tooltip.menu', '메뉴'), children: _jsx(MenuTooltip, { columns: menuColumns, onClose: handleCloseMenu, size: "small" }) }))] }));
|
|
317
332
|
}
|
|
@@ -15,7 +15,7 @@ export interface PassCardProduct {
|
|
|
15
15
|
name?: string;
|
|
16
16
|
/** 기간(일). 지정 시 code/name보다 우선. */
|
|
17
17
|
durationDays?: number;
|
|
18
|
-
/** 가격. 숫자(원 단위) 또는 이미 포맷된
|
|
18
|
+
/** 가격. 숫자(원 단위) 또는 문자열 — 숫자만 든 문자열('12000')은 '₩12,000' 으로, 이미 포맷된 글자('7,500원')는 '₩7,500' 으로. */
|
|
19
19
|
price?: string | number;
|
|
20
20
|
/** 원가(취소선). 숫자(원 단위) 또는 문자열. */
|
|
21
21
|
originalPrice?: string | number;
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { toWonSymbol } from '../../utils/format';
|
|
2
|
-
|
|
2
|
+
import i18n from '../../i18n';
|
|
3
|
+
/** 기간을 못 뽑았을 때의 플랜 글자 — 지금 언어의 `passCard.planSuffix`(ko '이용권'). 전에는 한국어가 박혀 영어 화면에도 떴다. */
|
|
4
|
+
const 기본플랜 = () => i18n.t('passCard.planSuffix', '이용권');
|
|
5
|
+
/** 숫자(원) 또는 문자열 가격을 '₩12,345' 형태로 통일. 숫자만 든 문자열('12000'·'12,000')도 수로 보고 ₩ 를 붙인다
|
|
6
|
+
* — 전에는 문자열이면 '원' 이 붙은 것만 바꿔, DB 에서 글자로 온 '12000' 이 기호·자릿수 없이 떴다. */
|
|
3
7
|
function formatPrice(v) {
|
|
4
8
|
if (v === undefined || v === null || v === '')
|
|
5
9
|
return undefined;
|
|
10
|
+
if (typeof v === 'string' && /^\s*\d[\d,]*\s*$/.test(v)) {
|
|
11
|
+
const n = Number(v.replace(/[,\s]/g, ''));
|
|
12
|
+
if (Number.isFinite(n))
|
|
13
|
+
return toWonSymbol(`${n.toLocaleString('ko-KR')}원`);
|
|
14
|
+
}
|
|
6
15
|
return typeof v === 'number' ? toWonSymbol(`${v.toLocaleString('ko-KR')}원`) : toWonSymbol(v);
|
|
7
16
|
}
|
|
8
17
|
/** 상품 코드/이름에서 말귀(speechear) / 말귀매니저(manager) 판별. */
|
|
@@ -30,7 +39,7 @@ function derivePlan(p) {
|
|
|
30
39
|
const en = name.match(/(\d+)\s*-?\s*(day|year)s?\b/i);
|
|
31
40
|
if (en)
|
|
32
41
|
return en[2].toLowerCase() === 'year' ? `${en[1]}년` : `${en[1]}일`;
|
|
33
|
-
return name ||
|
|
42
|
+
return name || 기본플랜();
|
|
34
43
|
}
|
|
35
44
|
/**
|
|
36
45
|
* 상품코드 접미사(_30d, _7d, _1y 등)에서 기간(일)을 도출 — derivePlan과 같은
|
|
@@ -50,15 +50,24 @@ export interface ConfirmPopupProps {
|
|
|
50
50
|
};
|
|
51
51
|
/**
|
|
52
52
|
* 확인 클릭 시 실행. `close`로 직접 닫을 수 있음(async 지원).
|
|
53
|
-
* 기본은 onConfirm 완료 후 자동으로 닫힘(closeOnConfirm)
|
|
53
|
+
* 기본은 onConfirm 완료 후 자동으로 닫힘(closeOnConfirm) — **닫히면 onClose 도 불린다**(확인으로 닫혀도).
|
|
54
|
+
* 받은 `close()` 를 부르면 그것도 닫힘으로 센다(닫히는 동안 잠금 유지).
|
|
54
55
|
* 던지거나 거절되면 창이 남고 잠금이 풀린다 — 거절은 console.error 로 받는다.
|
|
55
56
|
* 안에서 부모가 창을 치우면 그 뒤 onClose 는 불리지 않는다.
|
|
56
57
|
*/
|
|
57
58
|
onConfirm: (close: () => void) => void | Promise<void>;
|
|
58
59
|
/** false면 자동으로 닫지 않음 — onConfirm이 전달받은 close로 닫힘 시점을 제어 (성공 시에만 닫기 등). 기본 true */
|
|
59
60
|
closeOnConfirm?: boolean;
|
|
60
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* 창이 닫힌 뒤 호출(보통 열림 상태 끄기). 취소·X·ESC·바탕으로 닫을 때뿐 아니라 **확인(또는 곁 단추)으로 닫힐 때도 불린다** —
|
|
63
|
+
* 확인 뒤의 일은 onConfirm 에서 하고, 여기서는 "창이 사라졌다" 만 처리한다. 확인과 취소를 가르려면 onConfirm 에서 표시해 둔다.
|
|
64
|
+
*/
|
|
61
65
|
onClose: () => void;
|
|
66
|
+
/**
|
|
67
|
+
* 선택. 열릴 때 포커스를 둘 단추 — `'close'`(머리의 X, 기본·지금 동작) · `'confirm'`(확인) · `'cancel'`(취소).
|
|
68
|
+
* 되돌릴 수 없는 일(삭제)은 기본이나 `'cancel'` 로 두어 Enter 한 번에 실행되지 않게 한다. 그 단추가 없거나 꺼져 있으면 기본 자리.
|
|
69
|
+
*/
|
|
70
|
+
initialFocus?: 'close' | 'confirm' | 'cancel';
|
|
62
71
|
/** portal 없이 부모 컨테이너에 제자리 렌더 (목업/데모용) */
|
|
63
72
|
inline?: boolean;
|
|
64
73
|
/** 패널 최대 가로폭. 확인창 기본은 'small'(480px) */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { cx } from '../../utils/cx';
|
|
4
|
-
import { useRef, useState } from 'react';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import Popup from './Popup';
|
|
7
7
|
import Panel, { PanelRow } from '../Panel';
|
|
@@ -12,8 +12,23 @@ import Panel, { PanelRow } from '../Panel';
|
|
|
12
12
|
* @public
|
|
13
13
|
*/
|
|
14
14
|
export default function ConfirmPopup(props) {
|
|
15
|
-
const { title, message, topContent, details, children, confirmLabel, cancelLabel, hideCancel = false, extraAction, confirmVariant = 'main', confirmDisabled = false, onConfirm, closeOnConfirm = true, onClose, inline, size = 'small', className, } = props;
|
|
15
|
+
const { title, message, topContent, details, children, confirmLabel, cancelLabel, hideCancel = false, extraAction, confirmVariant = 'main', confirmDisabled = false, onConfirm, closeOnConfirm = true, onClose, inline, size = 'small', className, initialFocus = 'close', } = props;
|
|
16
16
|
const { t } = useTranslation();
|
|
17
|
+
/* 열릴 때 포커스 자리 — Popup 이 먼저 첫 포커스 자리(X)로 옮긴 뒤(자식 효과가 먼저 돈다) 여기서 고른 단추로 옮긴다.
|
|
18
|
+
단추 차례는 [곁 단추?, 취소?, 확인] 이다. */
|
|
19
|
+
const 본문Ref = useRef(null);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (inline || initialFocus === 'close')
|
|
22
|
+
return;
|
|
23
|
+
const 판 = 본문Ref.current?.closest('.popup-panel');
|
|
24
|
+
const 단추들 = Array.from(판?.querySelectorAll('.popup-footer--buttons button') ?? []);
|
|
25
|
+
const 고른 = initialFocus === 'confirm' ? 단추들[단추들.length - 1]
|
|
26
|
+
: hideCancel ? undefined : 단추들[extraAction ? 1 : 0];
|
|
27
|
+
if (고른 && !고른.disabled)
|
|
28
|
+
고른.focus();
|
|
29
|
+
// 열릴 때 한 번만
|
|
30
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
31
|
+
}, []);
|
|
17
32
|
// 처리 중(async onConfirm) 재진입 방지 — Popup이 onClick을 await하지 않아 closeOnConfirm:false
|
|
18
33
|
// 흐름(결제/환불)에서 더블클릭 시 RPC가 2회 발화될 수 있어, 여기서 in-flight를 직접 막는다.
|
|
19
34
|
const busyRef = useRef(false);
|
|
@@ -98,5 +113,5 @@ export default function ConfirmPopup(props) {
|
|
|
98
113
|
});
|
|
99
114
|
},
|
|
100
115
|
},
|
|
101
|
-
], children: [topContent != null && _jsx("div", { className: "confirm-popup__top", children: topContent }), details && details.length > 0 && (_jsx(Panel, { children: details.map((d, i) => (_jsx(PanelRow, { label: d.label, children: _jsx("span", { className: "panel-row__text body-2", children: d.value }) }, i))) })), _jsx("p", { className: "body-2 confirm-popup__message", children: message }), children] }));
|
|
116
|
+
], children: [topContent != null && _jsx("div", { className: "confirm-popup__top", children: topContent }), details && details.length > 0 && (_jsx(Panel, { children: details.map((d, i) => (_jsx(PanelRow, { label: d.label, children: _jsx("span", { className: "panel-row__text body-2", children: d.value }) }, i))) })), _jsx("p", { ref: 본문Ref, className: "body-2 confirm-popup__message", children: message }), children] }));
|
|
102
117
|
}
|
|
@@ -8,6 +8,7 @@ import Button from '../Button';
|
|
|
8
8
|
import Checkbox from '../Checkbox';
|
|
9
9
|
import Badge from '../Badge';
|
|
10
10
|
import { deviceKindVariant } from '../../utils/deviceKind';
|
|
11
|
+
import { formatWornDate, wornDateSortKey } from '../../utils/wornDate';
|
|
11
12
|
/* 기기 종류 배지 이름은 지금 언어로 — 값은 저장된 한국어(보청기·인공와우)라 그대로 보이면 영어 화면에도 한국어였다.
|
|
12
13
|
색(deviceKindVariant)은 저장값으로 가른다. 모르는 값은 그대로 */
|
|
13
14
|
const 기기종류열쇠 = { '보청기': 'hearingAid', '인공와우': 'cochlearImplant' };
|
|
@@ -16,6 +17,7 @@ function 기기종류이름(device, t) {
|
|
|
16
17
|
return k ? t(`device.type.${k}`, { defaultValue: device }) : device;
|
|
17
18
|
}
|
|
18
19
|
import { useTablePaging, useTableSort } from '../../hooks/useTablePaging';
|
|
20
|
+
import { 칸순빈값뒤 } from '../../utils/textSort';
|
|
19
21
|
import './DeviceListPopup.css';
|
|
20
22
|
/**
|
|
21
23
|
* 정렬에 쓸 문자열. 날짜는 **자리수를 채워** 잰다 — '2026.5.2' 와 '2026.11.5' 를
|
|
@@ -24,9 +26,8 @@ import './DeviceListPopup.css';
|
|
|
24
26
|
function 정렬값(r, key) {
|
|
25
27
|
if (key !== 'wornAt')
|
|
26
28
|
return String(r[key] ?? '');
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return `${r.wornYear}-${r.wornMonth.padStart(2, '0')}-${r.wornDay.padStart(2, '0')}`;
|
|
29
|
+
/* 월·일이 null 로 오는 옛 기록이 있다 — padStart 에서 표가 통째로 터졌다 */
|
|
30
|
+
return wornDateSortKey(r.wornYear, r.wornMonth, r.wornDay);
|
|
30
31
|
}
|
|
31
32
|
export default function DeviceSideTable({ rows, onSelect, onDelete, onWear, onRelease, onAdd }) {
|
|
32
33
|
const { t } = useTranslation();
|
|
@@ -39,10 +40,11 @@ export default function DeviceSideTable({ rows, onSelect, onDelete, onWear, onRe
|
|
|
39
40
|
return rows;
|
|
40
41
|
const { key, asc } = 정렬;
|
|
41
42
|
return [...rows].sort((a, b) => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
if (key === 'worn') {
|
|
44
|
+
const cmp = Number(b.worn) - Number(a.worn); // 켜진 것이 먼저
|
|
45
|
+
return asc ? cmp : -cmp;
|
|
46
|
+
}
|
|
47
|
+
return 칸순빈값뒤(정렬값(a, key), 정렬값(b, key), asc); // 빈 칸(착용일 없음 등)은 오름·내림 모두 맨 뒤 — useTableSort 와 같은 규칙
|
|
46
48
|
});
|
|
47
49
|
}, [rows, 정렬]);
|
|
48
50
|
/* 개수 보기·쪽넘김은 방문 표와 같은 것을 쓴다 — 같은 자리에서 같은 뜻으로 읽히게 */
|
|
@@ -86,7 +88,7 @@ export default function DeviceSideTable({ rows, onSelect, onDelete, onWear, onRe
|
|
|
86
88
|
{ content: _jsx("span", { children: row.fitType || '-' }) },
|
|
87
89
|
{ content: _jsx("span", { children: row.productName || '-' }) },
|
|
88
90
|
{
|
|
89
|
-
content: _jsx("span", { className: "dlp-date", children: row.wornYear
|
|
91
|
+
content: _jsx("span", { className: "dlp-date", children: formatWornDate(row.wornYear, row.wornMonth, row.wornDay) || '-' }),
|
|
90
92
|
},
|
|
91
93
|
/* 삭제를 넘겨준 화면에서만 이 칸이 선다 — 안 넘기면 칸 자체가 없어,
|
|
92
94
|
못 하는 일을 내밀지 않는다. 눌림은 줄로 새지 않게 막는다(줄을 누르면
|
|
@@ -6,7 +6,7 @@ import Popup from './Popup';
|
|
|
6
6
|
import Table from '../Table';
|
|
7
7
|
import Toolbar from '../Toolbar';
|
|
8
8
|
import './ListPopup.css';
|
|
9
|
-
import {
|
|
9
|
+
import { 칸순빈값뒤 } from '../../utils/textSort';
|
|
10
10
|
import { cx } from '../../utils/cx';
|
|
11
11
|
/** @public */
|
|
12
12
|
export default function ListPopup(props) {
|
|
@@ -41,15 +41,15 @@ export default function ListPopup(props) {
|
|
|
41
41
|
if (!sortKey)
|
|
42
42
|
return filteredItems;
|
|
43
43
|
return [...filteredItems].sort((a, b) => {
|
|
44
|
-
const va = a[sortKey]
|
|
45
|
-
const vb = b[sortKey]
|
|
44
|
+
const va = a[sortKey];
|
|
45
|
+
const vb = b[sortKey];
|
|
46
46
|
if (va === vb)
|
|
47
47
|
return 0;
|
|
48
48
|
/* `<`·`>` 로 견주면 코드포인트 차례가 된다 — 한글끼리는 맞아떨어져 안 드러나다가
|
|
49
49
|
**한글과 영문이 섞이면** 어긋난다(라틴이 한글보다 앞이라 `fwef` 가
|
|
50
|
-
`검수테스트2` 위에 선다). 숫자 칸은 숫자로 견주는 것을 `칸순` 이 지킨다.
|
|
51
|
-
|
|
52
|
-
return
|
|
50
|
+
`검수테스트2` 위에 선다). 숫자 칸은 숫자로 견주는 것을 `칸순` 이 지킨다.
|
|
51
|
+
빈 값(null·undefined·'')은 오름·내림 모두 맨 뒤 — 전에는 '' 로 채워 오름차순 맨 앞에 섰다(useTableSort 와 같은 규칙). */
|
|
52
|
+
return 칸순빈값뒤(va, vb, sortAsc);
|
|
53
53
|
});
|
|
54
54
|
}, [items, search, sortKey, sortAsc, searchEnabled, effectiveSearchFn]);
|
|
55
55
|
const totalPages = Math.max(1, Math.ceil(filtered.length / Number(pageSize)));
|
|
@@ -80,6 +80,13 @@
|
|
|
80
80
|
opacity: 1;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/* 터치 기기에는 hover 가 없다 — :hover 로만 드러나 손가락으로는 삭제 단추가 보이지 않았다. 늘 보이게만 한다(색은 그대로). */
|
|
84
|
+
@media (hover: none) {
|
|
85
|
+
.load-table-popup__delete {
|
|
86
|
+
opacity: 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
83
90
|
/* ── 썸네일: 컨테이너 배경을 구분선 색으로, 셀은 흰색으로 gap이 격자선 역할 ── */
|
|
84
91
|
.load-table-popup__thumb {
|
|
85
92
|
display: grid;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useState, useMemo } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import Popup from './Popup';
|
|
6
6
|
import Button from '../Button';
|
|
@@ -38,7 +38,9 @@ const DeleteButton = ({ onClick, ariaLabel }) => {
|
|
|
38
38
|
/* 화면낭독기가 읽는 이름이라 사전에서 온다 — 여기 박아 두면 영어로 쓰는 사람에게
|
|
39
39
|
이 버튼만 '삭제' 로 읽힌다. */
|
|
40
40
|
const { t } = useTranslation();
|
|
41
|
-
return (_jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "x", variant: "transparent-lightGray", className: "load-table-popup__delete", onClick: onClick, ariaLabel: ariaLabel ?? t('common.delete', '삭제'),
|
|
41
|
+
return (_jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "x", variant: "transparent-lightGray", className: "load-table-popup__delete", onClick: onClick, ariaLabel: ariaLabel ?? t('common.delete', '삭제'),
|
|
42
|
+
/* 보이는 풍선은 '삭제' 그대로 — 이름(표 이름 + 삭제)은 낭독기용이다 */
|
|
43
|
+
showTooltipInfo: t('common.delete', '삭제') }));
|
|
42
44
|
};
|
|
43
45
|
export default function LoadTablePopup({ onLoad, onCancel, tables: tablesProp, onDelete: onDeleteProp }) {
|
|
44
46
|
const { t } = useTranslation();
|
|
@@ -65,8 +67,28 @@ export default function LoadTablePopup({ onLoad, onCancel, tables: tablesProp, o
|
|
|
65
67
|
/* 목록을 받아(tables) 보여 주는데 onDelete 가 없으면 지울 길이 없다 — 전에는 브라우저 저장소에서 같은 id 의 표를 지우고
|
|
66
68
|
보이는 목록은 그대로였다(보이지 않는 표를 지움). 그때는 삭제 단추를 두지 않는다. */
|
|
67
69
|
const canDelete = !!onDeleteProp || tablesProp === undefined;
|
|
70
|
+
/* **지운 뒤 초점을 옆 카드로.** 누른 삭제 단추가 카드와 함께 사라지면 초점이 문서 처음(body)으로 떨어져, 키보드·낭독기
|
|
71
|
+
사용자는 목록 처음부터 다시 찾아와야 했다. 다음 카드(없으면 이전 카드)로 옮긴다. 부모가 목록을 나중에 줄이는 경우
|
|
72
|
+
(tables + onDelete)도 있어, 지운 표가 목록에서 **빠진 뒤에** 옮긴다. */
|
|
73
|
+
const cardEls = useRef(new Map());
|
|
74
|
+
const cardRef = (id) => (el) => { if (el)
|
|
75
|
+
cardEls.current.set(id, el);
|
|
76
|
+
else
|
|
77
|
+
cardEls.current.delete(id); };
|
|
78
|
+
const 지운뒤초점 = useRef(null);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const p = 지운뒤초점.current;
|
|
81
|
+
if (!p || sorted.some(t => t.id === p.id))
|
|
82
|
+
return;
|
|
83
|
+
지운뒤초점.current = null;
|
|
84
|
+
const 곳 = [p.next, p.prev].find(id => id != null && sorted.some(t => t.id === id));
|
|
85
|
+
if (곳)
|
|
86
|
+
cardEls.current.get(곳)?.focus();
|
|
87
|
+
}, [sorted]);
|
|
68
88
|
const handleDelete = useCallback((id, e) => {
|
|
69
89
|
e.stopPropagation();
|
|
90
|
+
const i = sorted.findIndex(t => t.id === id);
|
|
91
|
+
지운뒤초점.current = { id, next: sorted[i + 1]?.id ?? null, prev: i > 0 ? sorted[i - 1].id : null };
|
|
70
92
|
if (onDeleteProp) {
|
|
71
93
|
onDeleteProp(id);
|
|
72
94
|
}
|
|
@@ -76,7 +98,7 @@ export default function LoadTablePopup({ onLoad, onCancel, tables: tablesProp, o
|
|
|
76
98
|
}
|
|
77
99
|
if (selected === id)
|
|
78
100
|
setSelected(null);
|
|
79
|
-
}, [selected, onDeleteProp]);
|
|
101
|
+
}, [selected, onDeleteProp, sorted]);
|
|
80
102
|
const handleSetViewMode = useCallback((v) => setViewMode(v), []);
|
|
81
103
|
const handleSetSortKey = useCallback((v) => setSortKey(v), []);
|
|
82
104
|
const handleLoadSelected = useCallback((animateThen) => {
|
|
@@ -87,7 +109,7 @@ export default function LoadTablePopup({ onLoad, onCancel, tables: tablesProp, o
|
|
|
87
109
|
const headerActions = useMemo(() => tables.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Dropdown, { options: VIEW_OPTIONS, value: viewMode, onChange: handleSetViewMode }), _jsx(Dropdown, { options: SORT_OPTIONS, value: sortKey, onChange: handleSetSortKey })] })) : undefined, [tables.length, viewMode, sortKey, handleSetViewMode, handleSetSortKey, VIEW_OPTIONS, SORT_OPTIONS]);
|
|
88
110
|
return (_jsx(Popup, { className: "popup--load-table", title: t('wordTable.loadTable.title', '저장된 단어 표 불러오기'), headerActions: headerActions, onClose: onCancel, footer: ({ animateThen, closeWithAnimation }) => (_jsxs("div", { className: "load-table-popup__footer-actions", children: [_jsx(Button, { htmlType: "button", size: 48, type: "bar", label: t('common.cancel', '취소'), onClick: closeWithAnimation, width: "fit-content" }), _jsx(Button, { htmlType: "button", size: 48, type: "bar", variant: "main", label: t('wordTable.loadTable.loadButton', '불러오기'), width: "fit-content",
|
|
89
111
|
// 고른 표가 지금 목록에 있을 때만 — 부모 목록에서 빠진 표를 가리키면 눌러도 아무 일이 없었다
|
|
90
|
-
disabled: !selected || !tables.some(t => t.id === selected), onClick: () => handleLoadSelected(animateThen) })] })), children: _jsx("div", { className: "load-table-popup__body", children: tables.length === 0 ? (_jsx("p", { className: "load-table-popup__empty body-2", children: t('wordTable.loadTable.empty', '저장된 단어 표가 없습니다.') })) : (_jsxs(_Fragment, { children: [viewMode === 'grid' && (_jsx("ul", { className: "load-table-popup__grid", children: sorted.map(table => (_jsxs("li", { className: `load-table-popup__card${selected === table.id ? ' load-table-popup__card--selected' : ''}`, onClick: () => setSelected(table.id), ...pressable(() => setSelected(table.id)), children: [canDelete && _jsx(DeleteButton, { onClick: e => handleDelete(table.id, e), ariaLabel: t('wordTable.loadTable.deleteAriaLabel', '삭제') }), _jsx(TableThumbnail, { table: table }), _jsxs("div", { className: "load-table-popup__card-info", children: [_jsx("span", { className: "load-table-popup__card-name body-2", children: table.name }), _jsxs("span", { className: "load-table-popup__card-meta caption-1", children: [formatDate(table.savedAt), " \u00B7 ", t('wordTable.loadTable.itemCount', { count: table.items.length, defaultValue: '{{count}}개' })] })] })] }, table.id))) })), viewMode === 'list' && (_jsx("ul", { className: "load-table-popup__list", children: sorted.map(table => (_jsxs("li", { className: `load-table-popup__row${selected === table.id ? ' load-table-popup__row--selected' : ''}`, onClick: () => setSelected(table.id), ...pressable(() => setSelected(table.id)), children: [_jsx("span", { className: "load-table-popup__row-check", children: selected === table.id && (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", children: _jsx("polyline", { points: "4 13 9 18 20 6", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }) })) }), _jsxs("div", { className: "load-table-popup__row-main", children: [_jsx("span", { className: "load-table-popup__row-name body-2", children: table.name }), _jsx("div", { className: "load-table-popup__row-words", children: table.items.map((item, i) => (_jsx("span", { className: "load-table-popup__row-word caption-1", children: (item.words ?? []).length >= 2
|
|
112
|
+
disabled: !selected || !tables.some(t => t.id === selected), onClick: () => handleLoadSelected(animateThen) })] })), children: _jsx("div", { className: "load-table-popup__body", children: tables.length === 0 ? (_jsx("p", { className: "load-table-popup__empty body-2", children: t('wordTable.loadTable.empty', '저장된 단어 표가 없습니다.') })) : (_jsxs(_Fragment, { children: [viewMode === 'grid' && (_jsx("ul", { className: "load-table-popup__grid", children: sorted.map(table => (_jsxs("li", { ref: cardRef(table.id), className: `load-table-popup__card${selected === table.id ? ' load-table-popup__card--selected' : ''}`, onClick: () => setSelected(table.id), ...pressable(() => setSelected(table.id)), "aria-pressed": selected === table.id, children: [canDelete && _jsx(DeleteButton, { onClick: e => handleDelete(table.id, e), ariaLabel: t('wordTable.loadTable.deleteAriaLabel', { name: table.name, defaultValue: '{{name}} 삭제' }) }), _jsx(TableThumbnail, { table: table }), _jsxs("div", { className: "load-table-popup__card-info", children: [_jsx("span", { className: "load-table-popup__card-name body-2", children: table.name }), _jsxs("span", { className: "load-table-popup__card-meta caption-1", children: [formatDate(table.savedAt), " \u00B7 ", t('wordTable.loadTable.itemCount', { count: table.items.length, defaultValue: '{{count}}개' })] })] })] }, table.id))) })), viewMode === 'list' && (_jsx("ul", { className: "load-table-popup__list", children: sorted.map(table => (_jsxs("li", { ref: cardRef(table.id), className: `load-table-popup__row${selected === table.id ? ' load-table-popup__row--selected' : ''}`, onClick: () => setSelected(table.id), ...pressable(() => setSelected(table.id)), "aria-pressed": selected === table.id, children: [_jsx("span", { className: "load-table-popup__row-check", children: selected === table.id && (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", children: _jsx("polyline", { points: "4 13 9 18 20 6", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }) })) }), _jsxs("div", { className: "load-table-popup__row-main", children: [_jsx("span", { className: "load-table-popup__row-name body-2", children: table.name }), _jsx("div", { className: "load-table-popup__row-words", children: table.items.map((item, i) => (_jsx("span", { className: "load-table-popup__row-word caption-1", children: (item.words ?? []).length >= 2
|
|
91
113
|
? `${item.words[0].label} · ${item.words[1].label}`
|
|
92
|
-
: item.words?.[0]?.label ?? item.id }, i))) })] }), _jsx("span", { className: "load-table-popup__row-count caption-1", children: t('wordTable.loadTable.itemCount', { count: table.items.length, defaultValue: '{{count}}개' }) }), _jsx("span", { className: "load-table-popup__row-date caption-1", children: formatDate(table.savedAt) }), canDelete && _jsx(DeleteButton, { onClick: e => handleDelete(table.id, e), ariaLabel: t('wordTable.loadTable.deleteAriaLabel', '삭제') })] }, table.id))) }))] })) }) }));
|
|
114
|
+
: item.words?.[0]?.label ?? item.id }, i))) })] }), _jsx("span", { className: "load-table-popup__row-count caption-1", children: t('wordTable.loadTable.itemCount', { count: table.items.length, defaultValue: '{{count}}개' }) }), _jsx("span", { className: "load-table-popup__row-date caption-1", children: formatDate(table.savedAt) }), canDelete && _jsx(DeleteButton, { onClick: e => handleDelete(table.id, e), ariaLabel: t('wordTable.loadTable.deleteAriaLabel', { name: table.name, defaultValue: '{{name}} 삭제' }) })] }, table.id))) }))] })) }) }));
|
|
93
115
|
}
|
|
@@ -3,6 +3,14 @@ import { type ButtonVariant } from "../Button";
|
|
|
3
3
|
import "./Popup.css";
|
|
4
4
|
/** @public */
|
|
5
5
|
export type PopupOptions = {
|
|
6
|
+
/**
|
|
7
|
+
* 닫힘 애니메이션(260ms, '동작 줄이기' 면 0)을 시작하고 끝나면 `cb` 를 부른다.
|
|
8
|
+
*
|
|
9
|
+
* **창이 그 사이 떠나도(부모가 치워도) `cb` 는 부른다** — `cb` 가 삭제·저장 확정일 수 있어서다(결정 2026-09-15).
|
|
10
|
+
* 떠난 뒤에 부르면 곤란한 일(상태 되돌리기 등)은 `cb` 안에서 스스로 확인한다.
|
|
11
|
+
* 닫기(X·ESC·바탕)의 `onClose` 는 이와 달리 떠나면 부르지 않는다.
|
|
12
|
+
* `cb` 가 던지거나 돌려준 약속이 거절되면(또는 끝난 뒤에도 창이 남아 있으면) 창을 되살린다.
|
|
13
|
+
*/
|
|
6
14
|
animateThen: (cb?: () => void) => void;
|
|
7
15
|
closeWithAnimation: () => void;
|
|
8
16
|
};
|
|
@@ -9,6 +9,7 @@ import { lockBodyScroll } from "../../hooks/bodyScrollLock";
|
|
|
9
9
|
import "./Popup.css";
|
|
10
10
|
import { useFocusTrap, FOCUSABLE_SELECTOR } from '../../hooks/useFocusTrap';
|
|
11
11
|
import { rememberFocusReturn, resolveFocusReturn } from '../../hooks/focusReturn';
|
|
12
|
+
import { usePrefersReducedMotion } from '../../hooks/usePrefersReducedMotion';
|
|
12
13
|
const ANIM_MS = 260;
|
|
13
14
|
// 패널 안에서 Tab 순환 대상으로 삼을 요소들 — disabled/tabindex="-1"은 제외.
|
|
14
15
|
/** @public */
|
|
@@ -113,9 +114,11 @@ export default function Popup(props) {
|
|
|
113
114
|
전에는 닫는 중 표시만 남아 투명한 막이 화면을 덮고(클릭을 가로챔) ESC·X·바탕·다시 저장이 모두 무시돼 페이지를 쓸 수
|
|
114
115
|
없었다(말귀 업체 정보·훈련 묶음 수정 저장이 실패해 부모가 창을 열어 둘 때). 성공하면 부모가 곧 치우므로 되살릴 일이 없다. */
|
|
115
116
|
const 살아있음Ref = useRef(true);
|
|
117
|
+
/* '동작 줄이기' 면 닫기 대기를 0 으로 — CSS 는 움직임을 없애지만 이 타이머는 못 줄여, 움직임 없이 260ms 늦게 사라지며 투명한 막이 누름을 가로챘다 */
|
|
118
|
+
const 동작줄이기 = usePrefersReducedMotion();
|
|
116
119
|
/* 닫기(X·ESC·바탕)가 걸어 둔 타이머 — 닫는 애니메이션(260ms) 안에 부모가 창을 치우면 **떠난 뒤에 onClose** 가 불렸다(닫으며
|
|
117
120
|
다음 창을 여는 흐름에서 늦은 setX(null) 이 새 창을 닫을 수 있다). 떠날 때 걷는다. 소비자가 넘긴 animateThen(cb) 은
|
|
118
|
-
"삭제·저장 확정" 일 수 있어 걷지 않는다(결정
|
|
121
|
+
"삭제·저장 확정" 일 수 있어 걷지 않는다(결정 2026-09-15: 떠난 뒤에도 부른다 — PopupOptions JSDoc). */
|
|
119
122
|
const 닫기타이머Ref = useRef(null);
|
|
120
123
|
useEffect(() => {
|
|
121
124
|
살아있음Ref.current = true;
|
|
@@ -159,10 +162,10 @@ export default function Popup(props) {
|
|
|
159
162
|
if (결과 && typeof 결과.then === 'function') {
|
|
160
163
|
결과.then(() => { setTimeout(되살리기, 120); }, 되살리기);
|
|
161
164
|
}
|
|
162
|
-
}, ANIM_MS);
|
|
165
|
+
}, 동작줄이기 ? 0 : ANIM_MS);
|
|
163
166
|
if (닫기뿐)
|
|
164
167
|
닫기타이머Ref.current = id;
|
|
165
|
-
}, [
|
|
168
|
+
}, [되살리기, 동작줄이기]);
|
|
166
169
|
const closeWithAnimation = useCallback(() => {
|
|
167
170
|
if (closeDisabled)
|
|
168
171
|
return;
|
|
@@ -11,9 +11,13 @@ interface SaveTablePopupProps {
|
|
|
11
11
|
preview?: ReactNode;
|
|
12
12
|
previewW?: number;
|
|
13
13
|
previewH?: number;
|
|
14
|
+
/** @deprecated 읽지 않는다 — 미리보기는 `preview` 로 넘긴다. */
|
|
14
15
|
items?: WordSearchResultItem[];
|
|
15
16
|
existingNames?: string[];
|
|
16
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated 읽지 않는다(중복 이름 감지는 `existingNames` 만 본다) — 이름 목록을 `existingNames` 로 넘긴다.
|
|
19
|
+
* 전에는 "없으면 localStorage 에서 로드" 한다고 적혀 있었지만 그런 동작은 없다.
|
|
20
|
+
*/
|
|
17
21
|
existingTables?: {
|
|
18
22
|
name: string;
|
|
19
23
|
items: WordSearchResultItem[];
|
|
@@ -177,6 +177,9 @@
|
|
|
177
177
|
background: var(--color-white);
|
|
178
178
|
}
|
|
179
179
|
.trp-actions { display: flex; gap: var(--spacing-s); }
|
|
180
|
+
/* 결과 계산 중 '완료' 를 막은 까닭 — 단추 줄 위 한 줄 전체 */
|
|
181
|
+
.popup-footer.trp-result__foot { flex-wrap: wrap; }
|
|
182
|
+
.trp-actions__reason { flex-basis: 100%; margin: 0; text-align: center; }
|
|
180
183
|
|
|
181
184
|
/* ── Animations ── */
|
|
182
185
|
@keyframes result-rise {
|
|
@@ -8,6 +8,11 @@ interface TrainingResultPopupProps {
|
|
|
8
8
|
onClose?: () => void;
|
|
9
9
|
/** true면 portal 없이 부모 컨테이너 안에 제자리 렌더 (목업/데모용) */
|
|
10
10
|
inline?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* 결과를 아직 계산하는 중인가. true 인 동안 '완료' 단추를 막고(aria-disabled, 누르거나 Esc 로 닫아도 기록하지 않음)
|
|
13
|
+
* 까닭 문구를 보인다 — 결과 없이 완료하면 문항 수(total)가 비어 기록됐다. TrainingPagePopup 이 넘긴다. 기본 false.
|
|
14
|
+
*/
|
|
15
|
+
pending?: boolean;
|
|
11
16
|
}
|
|
12
17
|
/** @public */
|
|
13
18
|
export default function TrainingResultPopup(props: TrainingResultPopupProps): import("react").JSX.Element;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useEffect, useMemo } from "react";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useId, useMemo } from "react";
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import Button from '../Button';
|
|
6
6
|
import Badge from '../Badge';
|
|
@@ -58,8 +58,10 @@ function resolveDecision(it) {
|
|
|
58
58
|
}
|
|
59
59
|
/** @public */
|
|
60
60
|
export default function TrainingResultPopup(props) {
|
|
61
|
-
const { score, contextsLength, results = [], subtitle, onReplay, onClose, inline = false } = props || {};
|
|
61
|
+
const { score, contextsLength, results = [], subtitle, onReplay, onClose, inline = false, pending = false } = props || {};
|
|
62
62
|
const { t } = useTranslation();
|
|
63
|
+
const 까닭Id = useId();
|
|
64
|
+
const 계산중이면막기 = useCallback(() => !pending, [pending]);
|
|
63
65
|
// 축하 컨페티는 정답률 50% 초과일 때만 — 낮은 점수(≤50%)에 축하가 나가지 않게 한다(언어치료 맥락).
|
|
64
66
|
useEffect(() => {
|
|
65
67
|
const total = contextsLength ?? 0;
|
|
@@ -110,5 +112,9 @@ export default function TrainingResultPopup(props) {
|
|
|
110
112
|
/today · /training · /my-word-training · /my-homework). 화면에 보이는 그
|
|
111
113
|
제목을 그대로 접근명으로 쓴다. 새 문구를 만들면 눈으로 보는 이름과 읽어
|
|
112
114
|
주는 이름이 갈라진다. */
|
|
113
|
-
ariaLabel: t('training.result.title', '훈련을 모두 마쳤습니다.'), className: "popup--training-result",
|
|
115
|
+
ariaLabel: t('training.result.title', '훈련을 모두 마쳤습니다.'), className: "popup--training-result",
|
|
116
|
+
// 결과 계산 중에는 Esc 로도 완료(닫기)하지 않는다 — '완료' 단추와 같은 까닭
|
|
117
|
+
beforeClose: 계산중이면막기, footerClassName: "trp-result__foot", footer: ({ animateThen, closeWithAnimation }) => (_jsxs(_Fragment, { children: [pending && _jsx("p", { id: 까닭Id, className: "trp-actions__reason caption-1", role: "status", children: t('training.result.pendingDone', '결과를 계산하는 중이라 아직 완료할 수 없습니다.') }), _jsxs("div", { className: "trp-actions", children: [_jsx(Button, { htmlType: "button", size: 56, type: "bar", label: t('training.result.replay', '다시하기'), onClick: () => animateThen(onReplay) }), _jsx(Button, { htmlType: "button", size: 56, type: "bar", label: t('training.result.done', '완료'), variant: "main", onClick: closeWithAnimation,
|
|
118
|
+
/* 막되 초점은 남긴다(onDisabledClick 이 있으면 native disabled 대신 aria-disabled) — 키보드·낭독기가 까닭을 들을 수 있게 */
|
|
119
|
+
disabled: pending, onDisabledClick: pending ? () => { } : undefined, "aria-describedby": pending ? 까닭Id : undefined })] })] })), children: () => (_jsxs("div", { className: "trp-result is-on", children: [_jsxs("header", { className: "trp-result__head", children: [_jsx("p", { className: "trp-result__eyebrow caption-1", children: t('training.result.eyebrow', '훈련 완료') }), _jsx("h1", { className: "trp-result__title h2", children: t('training.result.title', '훈련을 모두 마쳤습니다.') }), subtitle && _jsx("p", { className: "trp-result__subtitle body-3", children: subtitle }), _jsx("div", { className: "trp-score", children: _jsxs("div", { className: "trp-score__figure", "aria-label": t('training.result.scoreAria', { correct, total }), children: [_jsx("span", { className: "trp-score__big", children: correct }), _jsx("span", { className: "trp-score__sep", children: "/" }), _jsx("span", { className: "trp-score__total", children: total })] }) })] }), rows.length > 0 && (_jsx("section", { className: "trp-result__body", children: _jsxs("div", { className: `result-table${hasAnswerCols ? ' result-table--answer' : ''}`, role: "table", "aria-label": t('training.result.tableAriaLabel', '문항별 판정'), children: [_jsxs("div", { className: "result-table__head caption-1", role: "row", children: [_jsx("span", { role: "columnheader", children: t('common.col.problem', '문제') }), hasAnswerCols && _jsx("span", { role: "columnheader", children: t('training.result.colAnswerSelected', '정답 · 선택') }), _jsx("span", { className: "col-right", role: "columnheader", children: t('training.result.colJudge', '판정') })] }), rows.map((r, i) => (_jsxs("div", { className: "result-table__row", role: "row", children: [_jsxs("div", { className: "trp-row__problem", children: [_jsx("span", { className: "trp-row__no caption-1", children: String(i + 1).padStart(2, '0') }), _jsx("div", { className: "trp-row__q body-2", children: r.problem })] }), hasAnswerCols && (_jsxs("div", { className: "trp-row__answers", children: [r.answer !== null && (_jsxs("div", { className: "trp-row__answer-line", children: [_jsx("span", { className: "trp-row__answer-label answer-correct caption-1", children: t('training.result.colAnswer', '정답') }), _jsx("span", { className: "trp-row__answer-value body-2", children: r.answer })] })), _jsxs("div", { className: "trp-row__answer-line", children: [_jsx("span", { className: `trp-row__answer-label answer-selected caption-1 ${r.isCorrect ? 'is-correct' : 'is-wrong'}`, children: t('training.result.colSelected', '선택') }), _jsx("span", { className: `trp-row__answer-value body-2 ${r.isCorrect ? 'is-correct' : 'is-wrong'}`, children: r.selectedText ?? '—' })] })] })), _jsx("div", { className: "trp-row__judge", children: _jsx(Badge, { variant: r.variant, label: r.label, fill: "soft", minWidth: 80 }) })] }, i)))] }) }))] })) }));
|
|
114
120
|
}
|
|
@@ -114,6 +114,15 @@
|
|
|
114
114
|
100% { transform: translateX(400%); }
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/* '동작 줄이기' — 전역 규칙이 반복을 1 회로 줄여 40% 채움으로 굳어 "40% 됐다" 로 보였다. 움직이지 않는 전체 폭으로 둔다 */
|
|
118
|
+
@media (prefers-reduced-motion: reduce) {
|
|
119
|
+
.wdup-bar__fill--indeterminate {
|
|
120
|
+
width: 100%;
|
|
121
|
+
animation: none !important;
|
|
122
|
+
transform: none;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
117
126
|
.wdup-phase-card__sub {
|
|
118
127
|
color: var(--color-lightGray);
|
|
119
128
|
}
|
|
@@ -46,6 +46,11 @@
|
|
|
46
46
|
생기므로 전환을 짧게 두되, 움직임을 줄여 달라는 설정은 따른다. */
|
|
47
47
|
.progress-indicator--dot {
|
|
48
48
|
gap: var(--spacing-xs);
|
|
49
|
+
/* 점이 많으면(검사 60문항 등) 한 줄이 부모보다 길어져, 옆의 "n / total" 머리줄을 화면 밖으로 밀었다(grid auto 칸).
|
|
50
|
+
줄바꿈을 허락하고 부모 폭 안에서 줄어들게 한다 — 한 줄에 다 들어가면 모양은 전과 같다. */
|
|
51
|
+
flex-wrap: wrap;
|
|
52
|
+
max-width: 100%;
|
|
53
|
+
min-width: 0;
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
.progress-indicator-dot--dot {
|
|
@@ -24,11 +24,13 @@ export interface ProgressIndicatorProps {
|
|
|
24
24
|
*/
|
|
25
25
|
variant?: 'numbered' | 'dot';
|
|
26
26
|
/**
|
|
27
|
-
* 단계가 바뀔 때 낭독기가 스스로 알리게 한다(`role="status"`).
|
|
27
|
+
* 단계가 바뀔 때 낭독기가 스스로 알리게 한다(`role="status"`). **모든 variant 에서** 된다.
|
|
28
28
|
*
|
|
29
29
|
* 기본은 알리지 않는다 — 점 모드는 그림에 가까워 `role="img"` + 이름표로
|
|
30
30
|
* 두는 편이 조용하다. 사용자가 넘기는 것이 아니라 화면이 저절로 넘어가는
|
|
31
31
|
* 자리(검사 안내)에서는 켜는 편이 낫다.
|
|
32
|
+
* 점 모드는 뿌리가 알리는 자리가 되고, 번호·모바일 모드는 목록(`role="list"`)을 그대로 두고
|
|
33
|
+
* 곁에 눈에 안 보이는 알리는 자리를 하나 둔다(목록 안에 두면 목록의 짜임이 깨진다).
|
|
32
34
|
*/
|
|
33
35
|
announce?: boolean;
|
|
34
36
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import useMediaQuery from '../hooks/useMediaQuery';
|
|
5
5
|
import './ProgressIndicator.css';
|
|
@@ -50,9 +50,11 @@ export default function ProgressIndicator({ max, index, inactiveBg, inactiveColo
|
|
|
50
50
|
if (variant === 'dot') {
|
|
51
51
|
return (_jsxs("div", { className: "progress-indicator progress-indicator--dot", style: rootStyle, role: announce ? 'status' : 'img', "aria-label": announce ? undefined : groupLabel, children: [announce && _jsx("span", { style: 보이지않게, children: groupLabel }), Array.from({ length: max }).map((_, i) => (_jsx("span", { "aria-hidden": "true", className: `progress-indicator-dot--dot${i < idx ? ' done' : i === idx ? ' active' : ''}` }, i)))] }));
|
|
52
52
|
}
|
|
53
|
+
/* 번호·모바일 모드의 알리는 자리 — 전에는 announce 가 점 모드에서만 먹었다. 목록 곁 형제로 두고 absolute 라 배치에 안 끼어든다. */
|
|
54
|
+
const 알림 = announce ? _jsx("span", { role: "status", style: 보이지않게, children: groupLabel }) : null;
|
|
53
55
|
// 모바일: 현재 단계만 표시 (n / max 형태)
|
|
54
56
|
if (isMobile) {
|
|
55
|
-
return (_jsx("div", { className: "progress-indicator progress-indicator--mobile", style: rootStyle, role: "list", "aria-label": groupLabel, children: _jsxs("span", { role: "listitem", className: "progress-indicator-dot active body-3", children: [idx + 1, " / ", max] }) }));
|
|
57
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "progress-indicator progress-indicator--mobile", style: rootStyle, role: "list", "aria-label": groupLabel, children: _jsxs("span", { role: "listitem", className: "progress-indicator-dot active body-3", children: [idx + 1, " / ", max] }) }), 알림] }));
|
|
56
58
|
}
|
|
57
|
-
return (_jsx("div", { className: "progress-indicator", style: rootStyle, role: "list", "aria-label": groupLabel, children: Array.from({ length: max }).map((_, i) => (_jsx("span", { role: "listitem", "aria-current": i === idx ? 'step' : undefined, className: `progress-indicator-dot ${i < idx ? ' done' : i === idx ? ' active' : ''} body-3`, children: i + 1 }, i))) }));
|
|
59
|
+
return (_jsxs(_Fragment, { children: [알림, _jsx("div", { className: "progress-indicator", style: rootStyle, role: "list", "aria-label": groupLabel, children: Array.from({ length: max }).map((_, i) => (_jsx("span", { role: "listitem", "aria-current": i === idx ? 'step' : undefined, className: `progress-indicator-dot ${i < idx ? ' done' : i === idx ? ' active' : ''} body-3`, children: i + 1 }, i))) })] }));
|
|
58
60
|
}
|