@ihabdevteam/core 0.321.0 → 0.323.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +617 -0
- package/dist/components.cjs.js +183 -183
- package/dist/components.esm.css +1 -1
- package/dist/hooks.cjs.js +3 -3
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core.cjs.js +187 -187
- package/dist/ihabdevteam-core.d.ts +108 -66
- package/dist/packages/core/src/components/Accordion.js +3 -3
- package/dist/packages/core/src/components/Alert.js +2 -2
- package/dist/packages/core/src/components/BarChart.js +120 -119
- package/dist/packages/core/src/components/BottomNav.js +18 -18
- package/dist/packages/core/src/components/BulkBar.js +25 -25
- package/dist/packages/core/src/components/Button.js +16 -16
- package/dist/packages/core/src/components/Calendar.js +17 -17
- package/dist/packages/core/src/components/ChartSection.js +30 -30
- package/dist/packages/core/src/components/CountTile.js +38 -38
- package/dist/packages/core/src/components/DatePicker.js +59 -59
- package/dist/packages/core/src/components/Dialog.js +14 -14
- package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.js +8 -8
- package/dist/packages/core/src/components/FilterPanel.js +6 -6
- package/dist/packages/core/src/components/FilterSectionInfo.js +6 -6
- package/dist/packages/core/src/components/GameIntro.js +1 -1
- package/dist/packages/core/src/components/HearingDeviceInfo.d.ts +5 -5
- package/dist/packages/core/src/components/HearingDeviceInfo.js +2 -2
- package/dist/packages/core/src/components/Icon.js +2 -2
- package/dist/packages/core/src/components/ItemActions.js +16 -16
- package/dist/packages/core/src/components/LandingHeader.js +6 -6
- package/dist/packages/core/src/components/LineChart.js +24 -24
- package/dist/packages/core/src/components/Loader.js +5 -5
- package/dist/packages/core/src/components/NoticeBox.js +3 -3
- package/dist/packages/core/src/components/PageTitle.js +2 -2
- package/dist/packages/core/src/components/Pagination.js +4 -4
- package/dist/packages/core/src/components/Panel.js +2 -2
- package/dist/packages/core/src/components/PanelRow.d.ts +2 -4
- package/dist/packages/core/src/components/PanelRow.js +8 -15
- package/dist/packages/core/src/components/PassCard/PassCard.js +9 -9
- package/dist/packages/core/src/components/PassCard/passCardProduct.js +2 -2
- package/dist/packages/core/src/components/PaymentPlan.js +5 -5
- package/dist/packages/core/src/components/Popup/ConfirmPopup.js +29 -29
- package/dist/packages/core/src/components/Popup/CouponInputPopup.js +2 -2
- package/dist/packages/core/src/components/Popup/DeviceListPopup.d.ts +4 -8
- package/dist/packages/core/src/components/Popup/DeviceListPopup.js +1 -1
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +19 -26
- package/dist/packages/core/src/components/Popup/ListPopup.js +6 -6
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +10 -10
- package/dist/packages/core/src/components/Popup/Popup.js +27 -27
- package/dist/packages/core/src/components/Popup/SaveTablePopup.js +5 -5
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +4 -4
- package/dist/packages/core/src/components/Popup/customerDetailUtils.js +18 -18
- package/dist/packages/core/src/components/Popup/useDiscardGuard.js +4 -4
- package/dist/packages/core/src/components/ProgressIndicator.js +5 -5
- package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +16 -16
- package/dist/packages/core/src/components/Rating.js +20 -20
- package/dist/packages/core/src/components/Report/ReportDocument.js +21 -21
- package/dist/packages/core/src/components/RingChart.js +5 -5
- package/dist/packages/core/src/components/SectionHeadFloat.js +15 -15
- package/dist/packages/core/src/components/SegmentedControl.js +34 -34
- package/dist/packages/core/src/components/SideNav.js +37 -37
- package/dist/packages/core/src/components/Slider.js +2 -2
- package/dist/packages/core/src/components/SoundCompass.js +5 -5
- package/dist/packages/core/src/components/Table.js +257 -98
- package/dist/packages/core/src/components/Tabs.js +12 -12
- package/dist/packages/core/src/components/TextField.js +34 -34
- package/dist/packages/core/src/components/Toast.js +6 -6
- package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +4 -4
- package/dist/packages/core/src/components/Tooltip/Tooltip.js +22 -22
- package/dist/packages/core/src/components/Tooltip/TrainingTooltip.js +4 -4
- package/dist/packages/core/src/components/TopBanner.js +28 -28
- package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +11 -11
- package/dist/packages/core/src/components/Training/CategoryIcon.js +2 -2
- package/dist/packages/core/src/components/Training/NoisePlayerContext.d.ts +7 -4
- package/dist/packages/core/src/components/Training/NoisePlayerContext.js +12 -12
- package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingBlock.js +12 -12
- package/dist/packages/core/src/components/Training/TrainingBlockButton.js +10 -10
- package/dist/packages/core/src/components/Training/TrainingBlockGrid.js +21 -21
- package/dist/packages/core/src/components/Training/TrainingCard.css +13 -1
- package/dist/packages/core/src/components/Training/TrainingCard.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingCardGroup.js +6 -6
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +83 -83
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +17 -17
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +128 -128
- package/dist/packages/core/src/components/Training/TrainingStepCard.js +18 -18
- package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +4 -4
- package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +12 -12
- package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +13 -13
- package/dist/packages/core/src/components/Training/TrainingStepTitle.js +4 -4
- package/dist/packages/core/src/components/Training/TrainingWordCard.js +17 -17
- package/dist/packages/core/src/components/Training/WordTablePopup.js +5 -5
- package/dist/packages/core/src/components/Training/audioPlaybackRules.js +8 -8
- package/dist/packages/core/src/components/Training/audioTimeline.js +25 -25
- package/dist/packages/core/src/components/Training/oneShotSound.d.ts +1 -1
- package/dist/packages/core/src/components/Training/oneShotSound.js +14 -14
- package/dist/packages/core/src/components/Training/stepAnswerable.d.ts +2 -2
- package/dist/packages/core/src/components/Training/trainingMissStats.js +22 -22
- package/dist/packages/core/src/components/Training/trainingPopupUtils.js +7 -7
- package/dist/packages/core/src/components/Training/useAudioPreview.js +15 -15
- package/dist/packages/core/src/components/Training/useFloatingFocus.js +14 -14
- package/dist/packages/core/src/components/Training/useNoiseFade.js +22 -22
- package/dist/packages/core/src/components/Training/useObjectUrls.js +2 -2
- package/dist/packages/core/src/components/Training/usePlaybackSequencer.d.ts +2 -2
- package/dist/packages/core/src/components/Training/usePlaybackSequencer.js +23 -23
- package/dist/packages/core/src/components/Training/useProgressTicker.js +33 -33
- package/dist/packages/core/src/components/Training/useTTSPlayback.js +17 -17
- package/dist/packages/core/src/components/Training/useWaveReady.js +11 -11
- package/dist/packages/core/src/components/UpdateAlert.js +6 -6
- package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +7 -1
- package/dist/packages/core/src/components/Word/TableFilterPanel.js +3 -3
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +18 -18
- package/dist/packages/core/src/components/Word/WordTableBlock.js +28 -28
- package/dist/packages/core/src/components/Word/WordTableButton.css +16 -2
- package/dist/packages/core/src/components/Word/WordTableThumbnail.css +6 -1
- package/dist/packages/core/src/components/Word/generatedDraft.js +14 -14
- package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -7
- package/dist/packages/core/src/components/Word/wordScoreStats.js +17 -17
- package/dist/packages/core/src/components/Word/wordTablePrint.js +10 -10
- package/dist/packages/core/src/components/Word/wordTableRecommend.js +13 -13
- package/dist/packages/core/src/components/Word/wordTableReview.js +28 -28
- package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +8 -11
- package/dist/packages/core/src/components/Word/wordTableSettings.js +48 -36
- package/dist/packages/core/src/components/WordTable.js +130 -130
- package/dist/packages/core/src/components/barChartData.d.ts +16 -1
- package/dist/packages/core/src/components/barChartData.js +49 -29
- package/dist/packages/core/src/components/barChartTypes.js +8 -8
- package/dist/packages/core/src/components/tableTrackList.js +11 -11
- package/dist/packages/core/src/components/updateAlertStack.js +28 -28
- package/dist/packages/core/src/components/useBarChartControl.js +4 -6
- package/dist/packages/core/src/components/useComboboxSuggestions.js +17 -17
- package/dist/packages/core/src/components/useToastLifecycle.js +18 -18
- package/dist/packages/core/src/components/wordTableLayout.d.ts +22 -7
- package/dist/packages/core/src/components/wordTableLayout.js +5 -5
- package/dist/packages/core/src/context/I18nProvider.js +6 -6
- package/dist/packages/core/src/hooks/focusReturn.js +27 -27
- package/dist/packages/core/src/hooks/inputModality.d.ts +1 -1
- package/dist/packages/core/src/hooks/inputModality.js +14 -14
- package/dist/packages/core/src/hooks/touchToggle.js +39 -39
- package/dist/packages/core/src/hooks/useAnchoredMenu.js +30 -30
- package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +28 -28
- package/dist/packages/core/src/hooks/useDeviceManagement.d.ts +5 -17
- package/dist/packages/core/src/hooks/useDeviceManagement.js +20 -8
- package/dist/packages/core/src/hooks/useDocumentTitle.js +10 -10
- package/dist/packages/core/src/hooks/useEditableList.js +4 -4
- package/dist/packages/core/src/hooks/useFocusTrap.js +2 -2
- package/dist/packages/core/src/hooks/useFontScale.js +6 -6
- package/dist/packages/core/src/hooks/useIcon.js +7 -7
- package/dist/packages/core/src/hooks/useListControls.js +2 -2
- package/dist/packages/core/src/hooks/usePageResetOnChange.d.ts +3 -3
- package/dist/packages/core/src/hooks/usePageResetOnChange.js +6 -6
- package/dist/packages/core/src/hooks/usePasswordChange.js +4 -4
- package/dist/packages/core/src/hooks/usePendingPress.js +7 -7
- package/dist/packages/core/src/hooks/useRelativeDay.js +4 -4
- package/dist/packages/core/src/hooks/useSoundCompassExam.js +6 -6
- package/dist/packages/core/src/hooks/useTablePaging.d.ts +7 -7
- package/dist/packages/core/src/hooks/useTablePaging.js +40 -34
- package/dist/packages/core/src/hooks/useTrainingSessionLog.js +35 -35
- package/dist/packages/core/src/hooks/useVisitSchedule.js +4 -4
- package/dist/packages/core/src/hooks/useWordAttributeFilter.js +14 -14
- package/dist/packages/core/src/hooks/useWordMeanings.js +5 -5
- package/dist/packages/core/src/hooks/useWordTrainingFilter.d.ts +3 -1
- package/dist/packages/core/src/hooks/useWordTrainingFilter.js +14 -12
- package/dist/packages/core/src/i18n/locales/en.json +13 -2
- package/dist/packages/core/src/i18n/locales/ko.json +12 -1
- package/dist/packages/core/src/types/wordSearch.d.ts +9 -0
- package/dist/packages/core/src/utils/audioMeter.js +5 -5
- package/dist/packages/core/src/utils/audioPeaks.d.ts +1 -1
- package/dist/packages/core/src/utils/audioPeaks.js +18 -18
- package/dist/packages/core/src/utils/blockGrid.js +4 -4
- package/dist/packages/core/src/utils/chunkSkew.js +9 -9
- package/dist/packages/core/src/utils/clipboard.js +3 -3
- package/dist/packages/core/src/utils/dailySelectionUtils.js +12 -12
- package/dist/packages/core/src/utils/date.js +12 -12
- package/dist/packages/core/src/utils/devWarn.d.ts +2 -2
- package/dist/packages/core/src/utils/devWarn.js +7 -7
- package/dist/packages/core/src/utils/deviceKind.d.ts +37 -7
- package/dist/packages/core/src/utils/deviceKind.js +53 -10
- package/dist/packages/core/src/utils/errorMessage.js +7 -7
- package/dist/packages/core/src/utils/filterPanelSummary.d.ts +1 -1
- package/dist/packages/core/src/utils/filterPanelSummary.js +16 -16
- package/dist/packages/core/src/utils/format.js +4 -4
- package/dist/packages/core/src/utils/getTrainingData.js +9 -9
- package/dist/packages/core/src/utils/imageConfig.js +17 -17
- package/dist/packages/core/src/utils/koreanSyllable.js +18 -18
- package/dist/packages/core/src/utils/navScroll.d.ts +2 -2
- package/dist/packages/core/src/utils/navScroll.js +6 -6
- package/dist/packages/core/src/utils/phonemeConvert.d.ts +7 -0
- package/dist/packages/core/src/utils/phonemeConvert.js +19 -10
- package/dist/packages/core/src/utils/phonemeGroups.js +2 -2
- package/dist/packages/core/src/utils/phonemePairSearch.d.ts +1 -0
- package/dist/packages/core/src/utils/phonemePairSearch.js +118 -102
- package/dist/packages/core/src/utils/phonemeSectionCodes.d.ts +52 -0
- package/dist/packages/core/src/utils/phonemeSectionCodes.js +76 -0
- package/dist/packages/core/src/utils/prefetchChunk.js +6 -6
- package/dist/packages/core/src/utils/pressable.d.ts +1 -1
- package/dist/packages/core/src/utils/pressable.js +3 -3
- package/dist/packages/core/src/utils/safeHref.js +4 -4
- package/dist/packages/core/src/utils/savedWordTables.js +5 -5
- package/dist/packages/core/src/utils/sessionTab.d.ts +7 -7
- package/dist/packages/core/src/utils/sessionTab.js +9 -9
- package/dist/packages/core/src/utils/speak.js +12 -12
- package/dist/packages/core/src/utils/textSort.d.ts +5 -5
- package/dist/packages/core/src/utils/textSort.js +47 -47
- package/dist/packages/core/src/utils/theme.js +3 -3
- package/dist/packages/core/src/utils/trainingItemResults.js +3 -3
- package/dist/packages/core/src/utils/ttsConfig.js +2 -2
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +2 -2
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +48 -48
- package/dist/packages/core/src/utils/wordContrast.js +3 -3
- package/dist/packages/core/src/utils/wordPairIndex.js +4 -4
- package/dist/packages/core/src/utils/wordTrainingGenerators.js +4 -4
- package/dist/packages/core/src/utils/wordTrainingPool.js +15 -15
- package/dist/packages/core/src/utils/wordTrainingRuntime.js +6 -6
- package/dist/packages/core/src/utils/wornDate.d.ts +3 -3
- package/dist/packages/core/src/utils/wornDate.js +14 -14
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +4 -4
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@ import Tooltip from './Tooltip/Tooltip';
|
|
|
7
7
|
import { useDelayedHoverAnchor } from '../hooks/useHoverAnchor';
|
|
8
8
|
/** 점 위에 잠깐 머물면 뜨는 뜸 — 지나가는 마우스에 툴팁이 번쩍이지 않을 만큼만 짧게 @public */
|
|
9
9
|
export const LINE_CHART_HOVER_DELAY_MS = 120;
|
|
10
|
-
const
|
|
10
|
+
const defaultInsets = { left: 38, right: 16, top: 26, bottom: 24 };
|
|
11
11
|
/**
|
|
12
12
|
* **작은 선 그래프** — 시간·조건에 따라 값이 어떻게 흐르는지를 그린다.
|
|
13
13
|
* 세로는 0~100%로 고정이고, 가로 범위는 부르는 쪽이 준다.
|
|
@@ -24,36 +24,36 @@ const 기본여백 = { left: 38, right: 16, top: 26, bottom: 24 };
|
|
|
24
24
|
export default function LineChart({ points = [], range, ticks = [], axisTitle, color, height = 156, fallbackWidth = 320, insets, overlay, ariaLabel, className, }) {
|
|
25
25
|
/* **제 상자를 재어 그 픽셀 수를 그림 칸으로 쓴다.** `width: 100%` 는 그대로라 viewBox 를 바꿔도
|
|
26
26
|
가로가 다시 바뀌지 않는다(되먹임이 없다). */
|
|
27
|
-
const [
|
|
28
|
-
const [
|
|
27
|
+
const [measuredWidth, setMeasuredWidth] = useState(null);
|
|
28
|
+
const [svgEl, setSvgEl] = useState(null);
|
|
29
29
|
useEffect(() => {
|
|
30
|
-
if (
|
|
30
|
+
if (!svgEl)
|
|
31
31
|
return;
|
|
32
|
-
const
|
|
33
|
-
|
|
32
|
+
const measure = (w) => setMeasuredWidth(Math.round(w) || null);
|
|
33
|
+
measure(svgEl.getBoundingClientRect().width);
|
|
34
34
|
if (typeof ResizeObserver === 'undefined')
|
|
35
35
|
return;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
return () => {
|
|
39
|
-
}, [
|
|
40
|
-
const { payload:
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
openAfterDelay(
|
|
36
|
+
const observer = new ResizeObserver(([e]) => measure(e.contentRect.width));
|
|
37
|
+
observer.observe(svgEl);
|
|
38
|
+
return () => { observer.disconnect(); };
|
|
39
|
+
}, [svgEl]);
|
|
40
|
+
const { payload: hoverText, anchorRect: hoverRect, openAfterDelay, close: close } = useDelayedHoverAnchor(LINE_CHART_HOVER_DELAY_MS);
|
|
41
|
+
const onHover = useCallback((text, el) => {
|
|
42
|
+
if (text)
|
|
43
|
+
openAfterDelay(text, el.getBoundingClientRect());
|
|
44
44
|
}, [openAfterDelay]);
|
|
45
|
-
const W =
|
|
46
|
-
const { left:
|
|
47
|
-
const [
|
|
45
|
+
const W = measuredWidth ?? fallbackWidth, H = height;
|
|
46
|
+
const { left: left, right: right, top: top, bottom: bottom } = { ...defaultInsets, ...insets };
|
|
47
|
+
const [first, last] = range;
|
|
48
48
|
/* 범위는 부르는 쪽이 적어도 한 칸 폭으로 준다(끝 > 처음) — 같으면 0 으로 나누므로 왼쪽 끝에 몰아 둔다 */
|
|
49
|
-
const x = (v) => (
|
|
50
|
-
const y = (pct) =>
|
|
51
|
-
const
|
|
49
|
+
const x = (v) => (last === first ? left : left + ((W - left - right) * (v - first)) / (last - first));
|
|
50
|
+
const y = (pct) => top + ((H - top - bottom) * (100 - pct)) / 100;
|
|
51
|
+
const scales = {
|
|
52
52
|
x, y,
|
|
53
|
-
plot: { left:
|
|
53
|
+
plot: { left: left, right: W - right, top: top, bottom: H - bottom, width: Math.max(0, W - left - right), height: Math.max(0, H - top - bottom) },
|
|
54
54
|
};
|
|
55
55
|
/* 선은 **믿을 점만** 잇는다 — 속 빈 점까지 이으면 "이만큼 봤다" 는 말이 흐려진다 */
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
return (_jsxs(_Fragment, { children: [_jsxs("svg", { ref:
|
|
56
|
+
const linePoints = points.filter(p => !p.hollow);
|
|
57
|
+
const strokeColor = color ?? 'var(--color-main)';
|
|
58
|
+
return (_jsxs(_Fragment, { children: [_jsxs("svg", { ref: setSvgEl, className: cx('line-chart', className), viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": ariaLabel ?? points.map(p => p.description).filter(Boolean).join(', '), children: [axisTitle && _jsx("text", { className: "line-chart__axis-title", x: 0, y: 12, children: axisTitle }), [0, 100].map(v => (_jsxs("g", { children: [_jsx("line", { className: "line-chart__grid", x1: left, x2: W - right, y1: y(v), y2: y(v) }), _jsxs("text", { className: "line-chart__tick", x: left - 6, y: y(v) + 4, textAnchor: "end", children: [v, "%"] })] }, v))), overlay?.(scales), ticks.map(tk => (_jsx("text", { className: "line-chart__tick", x: x(tk.x), y: H - 5, textAnchor: "middle", children: tk.label }, tk.x))), linePoints.length > 1 && (_jsx("polyline", { className: "line-chart__line", fill: "none", stroke: strokeColor, points: linePoints.map(p => `${x(p.x)},${y(p.pct)}`).join(' ') })), points.map(p => (_jsxs("g", { className: "line-chart__dot", onPointerEnter: e => onHover(p.description, e.currentTarget), onPointerLeave: close, children: [_jsx("circle", { className: "line-chart__dot-hit", cx: x(p.x), cy: y(p.pct), r: 12 }), _jsx("circle", { className: cx('line-chart__dot-main', p.hollow && 'is-hollow'), cx: x(p.x), cy: y(p.pct), r: 4, stroke: strokeColor, fill: p.hollow ? undefined : strokeColor })] }, p.x)))] }), hoverText != null && (_jsx(Tooltip, { anchorRect: hoverRect, offset: 10, closeOnOutsideClick: false, className: "line-chart__tip", role: "tooltip", ariaLabel: hoverText, children: hoverText }))] }));
|
|
59
59
|
}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import './Loader.css';
|
|
5
5
|
/** 눈에는 안 보이고 낭독기에는 읽히는 글자 */
|
|
6
|
-
const
|
|
6
|
+
const srOnly = {
|
|
7
7
|
position: 'absolute', width: 1, height: 1, margin: -1, padding: 0, overflow: 'hidden',
|
|
8
8
|
clipPath: 'inset(50%)', whiteSpace: 'nowrap', border: 0,
|
|
9
9
|
};
|
|
@@ -15,10 +15,10 @@ export default function Loader(props) {
|
|
|
15
15
|
const name = label || t('loader.loading', 'Loading');
|
|
16
16
|
if (variant === 'bar') {
|
|
17
17
|
/* 값을 모르면 0% 로 읽히고(빈 막대) 멈춘 채 보였다 — 값 없이 진행 중으로 둔다 */
|
|
18
|
-
const
|
|
19
|
-
const pct =
|
|
20
|
-
return (_jsx("div", { className: `loader-bar${
|
|
18
|
+
const unknown = progress == null || !Number.isFinite(Number(progress));
|
|
19
|
+
const pct = unknown ? 0 : Math.max(0, Math.min(100, Number(progress)));
|
|
20
|
+
return (_jsx("div", { className: `loader-bar${unknown ? ' loader-bar--indeterminate' : ''}`, role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": unknown ? undefined : pct, "aria-label": name, children: _jsx("div", { className: "loader-bar__inner", style: unknown ? undefined : { width: `${pct}%` } }) }));
|
|
21
21
|
}
|
|
22
22
|
const dim = typeof size === 'number' ? `${size}px` : size;
|
|
23
|
-
return (_jsxs("div", { className: "loader-spinner", style: { width: dim, height: dim }, role: "status", "aria-label": name, children: [_jsx("span", { style:
|
|
23
|
+
return (_jsxs("div", { className: "loader-spinner", style: { width: dim, height: dim }, role: "status", "aria-label": name, children: [_jsx("span", { style: srOnly, children: name }), _jsxs("svg", { className: "loader-spinner__svg", viewBox: "0 0 50 50", width: size, height: size, children: [_jsx("circle", { className: "loader-spinner__bg", cx: "25", cy: "25", r: "20", fill: "none", strokeWidth: "4" }), _jsx("circle", { className: "loader-spinner__fg", cx: "25", cy: "25", r: "20", fill: "none", strokeWidth: "4", strokeLinecap: "round" })] })] }));
|
|
24
24
|
}
|
|
@@ -19,12 +19,12 @@ export default function NoticeBox({ title = '', lines = [], notices = null, chil
|
|
|
19
19
|
if (dismissed)
|
|
20
20
|
return null;
|
|
21
21
|
const baseClass = `notice-box notice-box--${variant} body-2 ${className}`.trim();
|
|
22
|
-
const
|
|
22
|
+
const closeButton = dismissible && (_jsx("button", {
|
|
23
23
|
// type="button" — 없으면 폼 안에서 닫기를 누르는 순간 폼이 제출된다
|
|
24
24
|
type: "button", className: "notice-box__dismiss", onClick: handleDismiss, "aria-label": t('common.close', '닫기'), children: "\u00D7" }));
|
|
25
25
|
if (notices && Array.isArray(notices) && notices.length) {
|
|
26
|
-
return (_jsxs("div", { className: baseClass, role: "note", children: [notices.map((n, idx) => (_jsxs("div", { className: "notice-box__inner", children: [n.title ? _jsx("span", { className: "notice-box__title body-3", children: n.title }) : null, _jsx("div", { className: "notice-box__body", children: n.lines && n.lines.length ? (_jsx("div", { children: n.lines.map((l, i) => (_jsx("span", { className: "notice-box__line body-3", children: l }, i))) })) : (n.children ?? null) })] }, idx))),
|
|
26
|
+
return (_jsxs("div", { className: baseClass, role: "note", children: [notices.map((n, idx) => (_jsxs("div", { className: "notice-box__inner", children: [n.title ? _jsx("span", { className: "notice-box__title body-3", children: n.title }) : null, _jsx("div", { className: "notice-box__body", children: n.lines && n.lines.length ? (_jsx("div", { children: n.lines.map((l, i) => (_jsx("span", { className: "notice-box__line body-3", children: l }, i))) })) : (n.children ?? null) })] }, idx))), closeButton] }));
|
|
27
27
|
}
|
|
28
28
|
const content = (title || lines.length) ? (_jsxs("div", { className: "notice-box__inner", children: [title ? _jsx("span", { className: "notice-box__title body-1", children: title }) : null, _jsx("div", { className: "notice-box__body", children: lines.length ? (_jsx("ul", { children: lines.map((l, i) => (_jsx("li", { className: "notice-box__line", children: l }, i))) })) : children })] })) : (_jsx("div", { className: "notice-box__inner", children: children }));
|
|
29
|
-
return (_jsxs("div", { ref: rootRef, className: baseClass, role: "note", children: [content,
|
|
29
|
+
return (_jsxs("div", { ref: rootRef, className: baseClass, role: "note", children: [content, closeButton] }));
|
|
30
30
|
}
|
|
@@ -10,11 +10,11 @@ export default function PageTitle({ title, sub, rightAction, titleAction, badge,
|
|
|
10
10
|
const { t } = useTranslation();
|
|
11
11
|
// 페이지 제목이 곧 문서 제목이 되도록 맞춘다(WCAG 2.4.2).
|
|
12
12
|
useDocumentTitle(typeof title === 'string' ? title : (documentTitle ?? null));
|
|
13
|
-
const
|
|
13
|
+
const backText = backLabel ?? t('common.back', '뒤로');
|
|
14
14
|
return (_jsxs("div", { className: wrap ? 'page-title page-title--wrap' : 'page-title', children: [_jsxs("div", { className: "page-title__main", children: [_jsxs("div", { className: "page-title__title-row", children: [onBack && (
|
|
15
15
|
/* 글자 없이 화살표만 둔다 — 제목 옆에서 '뒤로' 라고 적으면 제목과 경쟁한다.
|
|
16
16
|
그만큼 과녁을 크게(48) 잡는다. 이름은 낭독기용으로 따로 준다. */
|
|
17
17
|
_jsx(Button, { htmlType: "button", className: "page-title__back", size: 48, type: "dot", icon: "chevron-left", iconSize: "large", useBorder: false,
|
|
18
18
|
/* 역할 이름을 쓴다 — `--color-black` 은 다크에서 이름과 실제 색이 뒤집힌다. */
|
|
19
|
-
iconColor: "var(--color-text-primary)", ariaLabel:
|
|
19
|
+
iconColor: "var(--color-text-primary)", ariaLabel: backText, showTooltipInfo: backText, onClick: onBack })), _jsx("h1", { className: "page-title__title h1", children: title }), badge && _jsx("div", { className: "page-title__badge", children: _jsx(Badge, { label: badge, size: "small", variant: badgeVariant, fill: "outline" }) }), titleAction ? _jsx("div", { className: "page-title__title-action", children: titleAction }) : null] }), sub && _jsx("p", { className: "page-title__sub body-2", children: sub })] }), rightAction ? _jsx("div", { className: "page-title__action", children: rightAction }) : null] }));
|
|
20
20
|
}
|
|
@@ -16,8 +16,8 @@ export default function Pagination({ page, totalPages, onChange, prevLabel, next
|
|
|
16
16
|
const resolvedNext = nextLabel ?? t('common.nextPage', '다음 페이지');
|
|
17
17
|
/* 범위 밖 쪽(page > totalPages — 결과가 줄어든 직후)에서도 '이전' 이 범위 안으로 돌아오게 먼저 조인다.
|
|
18
18
|
전에는 page-1 이 여전히 totalPages 보다 커 눌러도 범위 밖에 머물렀다. */
|
|
19
|
-
const
|
|
20
|
-
const handlePrev = () => onChange(Math.max(1, Math.min(
|
|
21
|
-
const handleNext = () => onChange(Math.min(
|
|
22
|
-
return (_jsxs("div", { className: cx('pagination', className), children: [_jsx(Button, { htmlType: "button", size: 32, type: "dot", shape: "rounded", icon: "chevron-left", ariaLabel: resolvedPrev, disabled: page <= 1, onClick: handlePrev }), _jsxs("span", { className: "pagination__info body-3", "aria-live": "polite", "aria-atomic": "true", children: [Math.min(Math.max(1, page),
|
|
19
|
+
const lastPage = Math.max(1, totalPages);
|
|
20
|
+
const handlePrev = () => onChange(Math.max(1, Math.min(lastPage, page - 1)));
|
|
21
|
+
const handleNext = () => onChange(Math.min(lastPage, page + 1));
|
|
22
|
+
return (_jsxs("div", { className: cx('pagination', className), children: [_jsx(Button, { htmlType: "button", size: 32, type: "dot", shape: "rounded", icon: "chevron-left", ariaLabel: resolvedPrev, disabled: page <= 1, onClick: handlePrev }), _jsxs("span", { className: "pagination__info body-3", "aria-live": "polite", "aria-atomic": "true", children: [Math.min(Math.max(1, page), lastPage), " / ", lastPage] }), _jsx(Button, { htmlType: "button", size: 32, type: "dot", shape: "rounded", icon: "chevron-right", ariaLabel: resolvedNext, disabled: page >= totalPages, onClick: handleNext })] }));
|
|
23
23
|
}
|
|
@@ -16,8 +16,8 @@ export default function Panel({ title, titleLevel = 2, icon, disc, actions, size
|
|
|
16
16
|
style.maxWidth = typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth;
|
|
17
17
|
if (labelWidth !== undefined)
|
|
18
18
|
style['--panel-label-width'] = typeof labelWidth === 'number' ? `${labelWidth}px` : labelWidth;
|
|
19
|
-
const
|
|
20
|
-
return (_jsxs("div", { className: cls, style: Object.keys(style).length ? style : undefined, children: [(!!title || disc != null) && (_jsxs("div", { className: cx('panel__header', disc != null ? 'panel__header--with-disc' : ''), children: [icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "panel__icon" })), _jsxs("div", { className: "panel__title-group", children: [!!title && (_jsx(
|
|
19
|
+
const TitleTag = titleLevel ? `h${titleLevel}` : 'div';
|
|
20
|
+
return (_jsxs("div", { className: cls, style: Object.keys(style).length ? style : undefined, children: [(!!title || disc != null) && (_jsxs("div", { className: cx('panel__header', disc != null ? 'panel__header--with-disc' : ''), children: [icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "panel__icon" })), _jsxs("div", { className: "panel__title-group", children: [!!title && (_jsx(TitleTag, { className: `panel__title ${disc != null ? 'body-3b' : 'h5b'}`, children: title })), disc != null && _jsx("p", { className: "panel__disc caption-1", children: disc })] }), actions && _jsx("div", { className: "panel__actions", children: actions })] })), loading ? (_jsx(PanelRow, { variant: "loading" })
|
|
21
21
|
/* 비었는지는 React 가 그리는 것으로 센다 — `{조건 && 행들}` 이 false 면 children 이 false 라 빈 상태 문구가 안 떴다 */
|
|
22
22
|
) : empty != null && Children.toArray(children).length === 0 ? (_jsx(PanelRow, { variant: "empty", children: empty })) : scrollable ? (_jsx("div", { className: cx('panel__body--scroll', scrollbarGutter === 'stable' && 'panel__body--gutter-stable'), children: children })) : children] }));
|
|
23
23
|
}
|
|
@@ -153,10 +153,8 @@ export type PanelRowProps = PanelRowToggle | PanelRowSegmented | PanelRowDropdow
|
|
|
153
153
|
export declare function PanelRow(props: PanelRowProps): import("react").JSX.Element;
|
|
154
154
|
export type { DeviceSide };
|
|
155
155
|
export interface PanelRowDeviceProps {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
우: DeviceEntry | null;
|
|
159
|
-
};
|
|
156
|
+
/** 쪽별로 지금 끼고 있는 기기 — 열쇠는 `DeviceSide` 값이다(`HearingDeviceInfo` 와 같은 꼴) */
|
|
157
|
+
devices: Record<DeviceSide, DeviceEntry | null>;
|
|
160
158
|
/** 있으면 각 쪽 이름 옆에 '착용 해제'가 선다 — 그 쪽의 '지금 착용중' 만 내린다(이력은 남는다) */
|
|
161
159
|
onRelease?: (side: DeviceSide) => void;
|
|
162
160
|
/**
|
|
@@ -8,15 +8,8 @@ import SegmentedControl from './SegmentedControl';
|
|
|
8
8
|
import Dropdown from './Dropdown';
|
|
9
9
|
import Button from './Button';
|
|
10
10
|
import Badge from './Badge';
|
|
11
|
-
import { deviceKindVariant } from '../utils/deviceKind';
|
|
11
|
+
import { deviceKindVariant, deviceKindLabel } from '../utils/deviceKind';
|
|
12
12
|
import { formatWornDate } from '../utils/wornDate';
|
|
13
|
-
/* 기기 종류 배지 이름은 지금 언어로 — 값은 저장된 한국어(보청기·인공와우)라 그대로 보이면 영어 화면에도 한국어였다.
|
|
14
|
-
색(deviceKindVariant)은 저장값으로 가른다. 모르는 값은 그대로 */
|
|
15
|
-
const 기기종류열쇠 = { '보청기': 'hearingAid', '인공와우': 'cochlearImplant' };
|
|
16
|
-
function 기기종류이름(device, t) {
|
|
17
|
-
const k = 기기종류열쇠[device];
|
|
18
|
-
return k ? t(`device.type.${k}`, { defaultValue: device }) : device;
|
|
19
|
-
}
|
|
20
13
|
import TextField from './TextField';
|
|
21
14
|
import Loader from './Loader';
|
|
22
15
|
import './Panel.css';
|
|
@@ -145,23 +138,23 @@ export function PanelRow(props) {
|
|
|
145
138
|
* 제목으로 이미 쓴 값과 빈 값, 그리고 서로 같은 값을 걷어 낸다. 걷어 내지 않으면
|
|
146
139
|
* 브랜드도 형태도 '기타' 인 기기가 **'기타 · 기타'** 로 떴다(같은 말이 두 번).
|
|
147
140
|
*/
|
|
148
|
-
function
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
return [...new Set(
|
|
141
|
+
function metaOf(d) {
|
|
142
|
+
const title = d.productName || d.brand;
|
|
143
|
+
const parts = [d.brand, d.fitType].filter((v) => !!v && v !== title);
|
|
144
|
+
return [...new Set(parts)].join(' · ');
|
|
152
145
|
}
|
|
153
146
|
export function PanelRowDevice({ devices, onRelease, emptyLabel }) {
|
|
154
147
|
const { t, i18n } = useTranslation(); /* i18n.language — 착용일이 보는 말을 따른다((118)) */
|
|
155
|
-
const SIDES = ['
|
|
148
|
+
const SIDES = ['left', 'right'];
|
|
156
149
|
return (_jsx("div", { className: "panel-row panel-row--device", children: SIDES.map(side => {
|
|
157
150
|
const d = devices[side];
|
|
158
|
-
const sideLabel = side === '
|
|
151
|
+
const sideLabel = side === 'left' ? t('device.direction.left', '좌') : t('device.direction.right', '우');
|
|
159
152
|
return (_jsxs("div", { className: "panel-row__device-column", children: [_jsxs("div", { className: "panel-row__device-head", children: [_jsx("span", { className: "panel-row__device-side body-2b", children: sideLabel }), d && onRelease && (
|
|
160
153
|
/* 표의 줄 끝 단추와 같은 모양·같은 말 — 하는 일도 같다.
|
|
161
154
|
이력에서 지우는 것이 아니라 '지금 끼고 있음' 만 내린다
|
|
162
155
|
(잠시 안 씀·수리 보냄 같은 경우가 실제로 있다). */
|
|
163
156
|
_jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "x",
|
|
164
157
|
// 좌·우 두 단추가 같은 이름이라 어느 쪽 기기를 내리는지 안 들렸다 — 이름에만 쪽을 붙인다(툴팁은 그대로)
|
|
165
|
-
ariaLabel: `${sideLabel} ${t('customers.device.unwear', '착용 해제')}`, showTooltipInfo: t('customers.device.unwear', '착용 해제'), onClick: () => onRelease(side) }))] }), d ? (_jsxs("div", { className: "panel-row__device-info", children: [_jsxs("span", { className: "panel-row__device-kind", children: [d.device && _jsx(Badge, { size: "small", variant: deviceKindVariant(d.device), fill: "soft", label:
|
|
158
|
+
ariaLabel: `${sideLabel} ${t('customers.device.unwear', '착용 해제')}`, showTooltipInfo: t('customers.device.unwear', '착용 해제'), onClick: () => onRelease(side) }))] }), d ? (_jsxs("div", { className: "panel-row__device-info", children: [_jsxs("span", { className: "panel-row__device-kind", children: [d.device && _jsx(Badge, { size: "small", variant: deviceKindVariant(d.device), fill: "soft", label: deviceKindLabel(d.device, t) }), metaOf(d) && _jsx("span", { className: "panel-row__device-detail body-3", children: metaOf(d) })] }), _jsx("span", { className: "panel-row__device-name body-2b", children: d.productName || d.brand }), formatWornDate(d.wornYear, d.wornMonth, d.wornDay, i18n.language) && (_jsx("span", { className: "panel-row__device-date caption-1", children: t('customers.device.wornSince', { date: formatWornDate(d.wornYear, d.wornMonth, d.wornDay, i18n.language) }) }))] })) : (_jsx("span", { className: "panel-row__device-empty body-3", children: emptyLabel ?? t('customers.device.notRegistered', '미등록') }))] }, side));
|
|
166
159
|
}) }));
|
|
167
160
|
}
|
|
@@ -71,7 +71,7 @@ const MAX_TILT = 10;
|
|
|
71
71
|
*
|
|
72
72
|
* `formatRemaining` 과 **같은 사다리**를 탄다. 한쪽만 고치면 또 갈리므로 바로 아래 붙여 둔다.
|
|
73
73
|
*/
|
|
74
|
-
function
|
|
74
|
+
function toDisplayUnit(seconds) {
|
|
75
75
|
if (seconds <= 0)
|
|
76
76
|
return 0;
|
|
77
77
|
if (seconds >= 86400)
|
|
@@ -169,10 +169,10 @@ export default function PassCard({ ariaLabel, product, showPrice = false, plan,
|
|
|
169
169
|
const end = new Date(endDate).getTime();
|
|
170
170
|
if (Number.isNaN(end))
|
|
171
171
|
return null;
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
const days =
|
|
175
|
-
? Math.round((Date.UTC(
|
|
172
|
+
const endMatch = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(endDate));
|
|
173
|
+
const todayMatch = /^(\d{4})-(\d{2})-(\d{2})/.exec(getTodayKst());
|
|
174
|
+
const days = endMatch && todayMatch
|
|
175
|
+
? Math.round((Date.UTC(+endMatch[1], +endMatch[2] - 1, +endMatch[3]) - Date.UTC(+todayMatch[1], +todayMatch[2] - 1, +todayMatch[3])) / 86_400_000)
|
|
176
176
|
: Math.round((end - Date.now()) / 86_400_000);
|
|
177
177
|
/* endInclusive — 종료일 당일까지 쓰는 이용권이면 그 날도 남은 날이다(마지막 날이 0초·0% 로 보이지 않게) */
|
|
178
178
|
remaining = Math.max(0, endInclusive ? days + 1 : days) * 86400;
|
|
@@ -192,8 +192,8 @@ export default function PassCard({ ariaLabel, product, showPrice = false, plan,
|
|
|
192
192
|
/* **옆에 적히는 글과 같은 값을 센다**(말귀 요청, 2026-10-02) — 자세한 까닭은
|
|
193
193
|
`적히는눈금으로` 머리글. 날짜(`endDate`)로 셈한 길은 이미 날 단위 정수라 이 걸음이
|
|
194
194
|
아무것도 바꾸지 않는다. 벌어지던 것은 초를 그대로 받는 `remainingSeconds` 쪽이다. */
|
|
195
|
-
const
|
|
196
|
-
const percent = Math.max(0, Math.min(100, Math.floor((Math.min(
|
|
195
|
+
const displayRemaining = toDisplayUnit(remaining);
|
|
196
|
+
const percent = Math.max(0, Math.min(100, Math.floor((Math.min(displayRemaining, totalSeconds) / totalSeconds) * 100)));
|
|
197
197
|
if (state === 'pending' && percent >= 100)
|
|
198
198
|
return null;
|
|
199
199
|
/* **만료는 '0초 남음' 이 아니다**(말귀 요청 ㊾). "0초 남았다" 는 **곧 끝난다**는 말로
|
|
@@ -301,8 +301,8 @@ export default function PassCard({ ariaLabel, product, showPrice = false, plan,
|
|
|
301
301
|
const handleCloseMenu = useCallback(() => setMenuAnchor(null), []);
|
|
302
302
|
const planDisplay = useMemo(() => {
|
|
303
303
|
/* 폴백 플랜('이용권'·영어 'Plan')은 큰 글씨에서 뺀다 — 아래 꼬리(planSuffix)에 이미 적힌다 */
|
|
304
|
-
const
|
|
305
|
-
const text = rPlan.replace('이용권', '').replace(
|
|
304
|
+
const planSuffix = t('passCard.planSuffix', '이용권');
|
|
305
|
+
const text = rPlan.replace('이용권', '').replace(planSuffix === '이용권' ? '' : planSuffix, '').trim();
|
|
306
306
|
const match = text.match(/^(.*?)(일|년)$/);
|
|
307
307
|
return match
|
|
308
308
|
? _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', '일') })] })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { toWonSymbol } from '../../utils/format';
|
|
2
2
|
import i18n from '../../i18n';
|
|
3
3
|
/** 기간을 못 뽑았을 때의 플랜 글자 — 지금 언어의 `passCard.planSuffix`(ko '이용권'). 전에는 한국어가 박혀 영어 화면에도 떴다. */
|
|
4
|
-
const
|
|
4
|
+
const planFallback = () => i18n.t('passCard.planSuffix', '이용권');
|
|
5
5
|
/** 숫자(원) 또는 문자열 가격을 '₩12,345' 형태로 통일. 숫자만 든 문자열('12000'·'12,000')도 수로 보고 ₩ 를 붙인다
|
|
6
6
|
* — 전에는 문자열이면 '원' 이 붙은 것만 바꿔, DB 에서 글자로 온 '12000' 이 기호·자릿수 없이 떴다. */
|
|
7
7
|
function formatPrice(v) {
|
|
@@ -39,7 +39,7 @@ function derivePlan(p) {
|
|
|
39
39
|
const en = name.match(/(\d+)\s*-?\s*(day|year)s?\b/i);
|
|
40
40
|
if (en)
|
|
41
41
|
return en[2].toLowerCase() === 'year' ? `${en[1]}년` : `${en[1]}일`;
|
|
42
|
-
return name ||
|
|
42
|
+
return name || planFallback();
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
45
|
* 상품코드 접미사(_30d, _7d, _1y 등)에서 기간(일)을 도출 — derivePlan과 같은
|
|
@@ -38,8 +38,8 @@ function renderPriceParts(raw) {
|
|
|
38
38
|
/** @public */
|
|
39
39
|
export default function PaymentPlan(props) {
|
|
40
40
|
const { t } = useTranslation();
|
|
41
|
-
const
|
|
42
|
-
const
|
|
41
|
+
const planFallback = t('payment.plan.tierFallback', { defaultValue: '이용권' });
|
|
42
|
+
const openPromoLabel = t('payment.plan.openPromo', { defaultValue: '오픈 특가' });
|
|
43
43
|
const { plan, onSelect, period = "monthly", ownedCodes } = props || {};
|
|
44
44
|
const owned = (code) => !!code && !!ownedCodes?.includes(code);
|
|
45
45
|
// 이 플랜의 오픈특가 tier를 이미 보유(구매)했는지 — 그러면 기본 tier를 카드로 승격
|
|
@@ -85,14 +85,14 @@ export default function PaymentPlan(props) {
|
|
|
85
85
|
/* 라벨이 없을 때의 대체말도 **화면에 뜨는 글**이다 — 사전에서 가져온다.
|
|
86
86
|
아래 정규식의 '오픈 특가' 는 다르다. 그건 앱이 넘겨준 **데이터를
|
|
87
87
|
읽는** 것이라 한국어 그대로여야 한다. */
|
|
88
|
-
const rawLabel = tier.label ||
|
|
88
|
+
const rawLabel = tier.label || planFallback;
|
|
89
89
|
// 라벨의 '오픈 특가'는 계정당 1회 제한의 표시다 — 숨김·소진 판정에만 쓴다.
|
|
90
90
|
const isOnceOnly = isOnceOnlyTier(tier);
|
|
91
91
|
// 배지 문구는 별도 — 같은 프로모션이되 1회 제한은 없는 tier(1년권)도 함께 얹는다.
|
|
92
92
|
const showOpenPromo = isOnceOnly || tier.openPromo === true;
|
|
93
|
-
const planText = rawLabel.replace(/^오픈\s*특가\s*/, '') ||
|
|
93
|
+
const planText = rawLabel.replace(/^오픈\s*특가\s*/, '') || planFallback;
|
|
94
94
|
const curPrice = tier.prices ? tier.prices[period] : tier.price;
|
|
95
|
-
const discount = tier.badge ? (showOpenPromo ? `${
|
|
95
|
+
const discount = tier.badge ? (showOpenPromo ? `${openPromoLabel}\n${tier.badge}` : tier.badge) : undefined;
|
|
96
96
|
// 이미 구매한 오픈특가(계정당 1회)는 결제 화면에서 숨긴다.
|
|
97
97
|
if (isOnceOnly && owned(tier.code))
|
|
98
98
|
return null;
|
|
@@ -16,16 +16,16 @@ export default function ConfirmPopup(props) {
|
|
|
16
16
|
const { t } = useTranslation();
|
|
17
17
|
/* 열릴 때 포커스 자리 — Popup 이 먼저 첫 포커스 자리(X)로 옮긴 뒤(자식 효과가 먼저 돈다) 여기서 고른 단추로 옮긴다.
|
|
18
18
|
단추 차례는 [곁 단추?, 취소?, 확인] 이다. */
|
|
19
|
-
const
|
|
19
|
+
const bodyRef = useRef(null);
|
|
20
20
|
useEffect(() => {
|
|
21
21
|
if (inline || initialFocus === 'close')
|
|
22
22
|
return;
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
: hideCancel ? undefined :
|
|
27
|
-
if (
|
|
28
|
-
|
|
23
|
+
const panel = bodyRef.current?.closest('.popup-panel');
|
|
24
|
+
const buttons = Array.from(panel?.querySelectorAll('.popup-footer--buttons button') ?? []);
|
|
25
|
+
const picked = initialFocus === 'confirm' ? buttons[buttons.length - 1]
|
|
26
|
+
: hideCancel ? undefined : buttons[extraAction ? 1 : 0];
|
|
27
|
+
if (picked && !picked.disabled)
|
|
28
|
+
picked.focus();
|
|
29
29
|
// 열릴 때 한 번만
|
|
30
30
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
31
31
|
}, []);
|
|
@@ -39,19 +39,19 @@ export default function ConfirmPopup(props) {
|
|
|
39
39
|
단추가 꺼지며 body 로 떨어진 포커스를 누른 단추로 되돌린다. */
|
|
40
40
|
/* closeOnConfirm=false 로 두고 콜백이 넘겨받은 close() 를 **스스로** 부르면(문서가 권하는 "성공했을 때만 닫기") 그것도 닫힘으로
|
|
41
41
|
센다 — 전에는 closeOnConfirm(false) 만 돌려 닫히는 260ms 동안 잠금이 풀려, 한 번 더 누르면 확인이 두 번 나갔다. */
|
|
42
|
-
const
|
|
43
|
-
let
|
|
44
|
-
return { close: () => {
|
|
42
|
+
const makeClose = (closeWithAnimation) => {
|
|
43
|
+
let closed = false;
|
|
44
|
+
return { close: () => { closed = true; closeWithAnimation(); }, didClose: () => closed };
|
|
45
45
|
};
|
|
46
|
-
const
|
|
46
|
+
const once = async (task) => {
|
|
47
47
|
if (busyRef.current)
|
|
48
48
|
return;
|
|
49
49
|
busyRef.current = true;
|
|
50
|
-
const
|
|
50
|
+
const pressedAt = typeof document !== 'undefined' ? document.activeElement : null;
|
|
51
51
|
setBusy(true);
|
|
52
|
-
let
|
|
52
|
+
let closing = false;
|
|
53
53
|
try {
|
|
54
|
-
|
|
54
|
+
closing = await task();
|
|
55
55
|
}
|
|
56
56
|
catch (e) {
|
|
57
57
|
/* 거절은 창을 남기고 잠금을 푸는 것으로 끝낸다(계약) — 받아 주지 않으면 처리되지 않은 거절로 새어 Next 개발 오버레이가 떴다 */
|
|
@@ -59,13 +59,13 @@ export default function ConfirmPopup(props) {
|
|
|
59
59
|
console.error(e);
|
|
60
60
|
}
|
|
61
61
|
finally {
|
|
62
|
-
if (
|
|
62
|
+
if (!closing) {
|
|
63
63
|
busyRef.current = false;
|
|
64
64
|
setBusy(false);
|
|
65
65
|
requestAnimationFrame(() => {
|
|
66
|
-
const
|
|
67
|
-
if ((
|
|
68
|
-
|
|
66
|
+
const active = document.activeElement;
|
|
67
|
+
if ((!active || active === document.body) && pressedAt?.isConnected)
|
|
68
|
+
pressedAt.focus();
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -85,13 +85,13 @@ export default function ConfirmPopup(props) {
|
|
|
85
85
|
// 없는 일(삭제·연결 해제)이라, 두 번 눌려 두 번 나가면 첫 번째만 뜻이 있고
|
|
86
86
|
// 두 번째는 엉뚱한 실패로 돌아온다.
|
|
87
87
|
onClick: async ({ closeWithAnimation }) => {
|
|
88
|
-
await
|
|
89
|
-
const
|
|
90
|
-
await extraAction.onClick(
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
88
|
+
await once(async () => {
|
|
89
|
+
const close = makeClose(closeWithAnimation);
|
|
90
|
+
await extraAction.onClick(close.close);
|
|
91
|
+
const closeOnClick = extraAction.closeOnClick ?? true;
|
|
92
|
+
if (closeOnClick)
|
|
93
93
|
closeWithAnimation();
|
|
94
|
-
return
|
|
94
|
+
return closeOnClick || close.didClose();
|
|
95
95
|
});
|
|
96
96
|
},
|
|
97
97
|
}] : []),
|
|
@@ -106,14 +106,14 @@ export default function ConfirmPopup(props) {
|
|
|
106
106
|
variant: confirmVariant,
|
|
107
107
|
disabled: confirmDisabled || busy,
|
|
108
108
|
onClick: async ({ closeWithAnimation }) => {
|
|
109
|
-
await
|
|
110
|
-
const
|
|
111
|
-
await onConfirm(
|
|
109
|
+
await once(async () => {
|
|
110
|
+
const close = makeClose(closeWithAnimation);
|
|
111
|
+
await onConfirm(close.close);
|
|
112
112
|
if (closeOnConfirm)
|
|
113
113
|
closeWithAnimation();
|
|
114
|
-
return closeOnConfirm ||
|
|
114
|
+
return closeOnConfirm || close.didClose();
|
|
115
115
|
});
|
|
116
116
|
},
|
|
117
117
|
},
|
|
118
|
-
], 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:
|
|
118
|
+
], 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: bodyRef, className: "body-2 confirm-popup__message", children: message }), children] }));
|
|
119
119
|
}
|
|
@@ -13,7 +13,7 @@ export default function CouponInputPopup({ onApply, onCancel }) {
|
|
|
13
13
|
const [error, setError] = useState(null);
|
|
14
14
|
const inputRef = useRef(null);
|
|
15
15
|
/* 글귀와 칸을 잇는 id — 한 쪽에 이 창이 둘 설 일은 없지만 id 는 문서에서 하나여야 한다 */
|
|
16
|
-
const
|
|
16
|
+
const hintId = useId();
|
|
17
17
|
// Popup 자체 포커스 로직(패널 내 첫 focusable = 헤더 닫기 버튼)보다 먼저 실행되도록
|
|
18
18
|
// 자식 effect가 부모보다 먼저 도는 순서를 이용해 입력창을 선점 포커스한다.
|
|
19
19
|
useEffect(() => { inputRef.current?.focus(); }, []);
|
|
@@ -46,5 +46,5 @@ export default function CouponInputPopup({ onApply, onCancel }) {
|
|
|
46
46
|
return (_jsx(Popup, { className: "coupon-input-popup", title: t('usage.couponButton', '쿠폰 입력'), size: "small", onClose: onCancel, closeDisabled: loading, buttons: [
|
|
47
47
|
{ label: t('common.cancel', '취소'), variant: 'default', disabled: loading, onClick: ({ closeWithAnimation }) => closeWithAnimation() },
|
|
48
48
|
{ label: t('coupons.applyButton', '적용'), variant: 'main', disabled: !code.trim() || loading, onClick: ({ closeWithAnimation }) => void handleApply(closeWithAnimation) },
|
|
49
|
-
], children: ({ closeWithAnimation }) => (_jsxs(_Fragment, { children: [_jsx("input", { ref: inputRef, className: "coupon-input-popup__input body-2", "aria-label": t('coupons.input.label', '쿠폰 코드'), "aria-invalid": error ? true : undefined, "aria-errormessage": error ?
|
|
49
|
+
], children: ({ closeWithAnimation }) => (_jsxs(_Fragment, { children: [_jsx("input", { ref: inputRef, className: "coupon-input-popup__input body-2", "aria-label": t('coupons.input.label', '쿠폰 코드'), "aria-invalid": error ? true : undefined, "aria-errormessage": error ? hintId : undefined, "aria-describedby": error ? hintId : undefined, placeholder: t('coupons.input.placeholder', '쿠폰 코드 입력'), value: code, onChange: handleChange, onKeyDown: e => handleKey(e, closeWithAnimation), disabled: loading, autoComplete: "off", spellCheck: false }), error && _jsx("p", { id: hintId, className: "coupon-input-popup__error caption-1", role: "alert", children: error })] })) }));
|
|
50
50
|
}
|
|
@@ -3,14 +3,10 @@ import type { DeviceSide } from '../Panel';
|
|
|
3
3
|
import type { DeviceForm } from '../../types/device';
|
|
4
4
|
import './DeviceListPopup.css';
|
|
5
5
|
interface DeviceListPopupProps {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
deviceHistory: {
|
|
11
|
-
좌: DeviceEntry[];
|
|
12
|
-
우: DeviceEntry[];
|
|
13
|
-
};
|
|
6
|
+
/** 쪽별로 지금 끼고 있는 기기 — 열쇠는 `DeviceSide` 값이다 */
|
|
7
|
+
devices: Record<DeviceSide, DeviceEntry | null>;
|
|
8
|
+
/** 쪽별로 바꿔 온 이력 */
|
|
9
|
+
deviceHistory: Record<DeviceSide, DeviceEntry[]>;
|
|
14
10
|
onClose: () => void;
|
|
15
11
|
onSelect: (d: DeviceForm) => void;
|
|
16
12
|
onDelete: (side: DeviceSide, index: number) => void;
|
|
@@ -16,7 +16,7 @@ import './DeviceListPopup.css';
|
|
|
16
16
|
*/
|
|
17
17
|
export default function DeviceListPopup({ devices, deviceHistory, onClose, onSelect, onDelete }) {
|
|
18
18
|
const { t } = useTranslation();
|
|
19
|
-
const rows = useMemo(() => ['
|
|
19
|
+
const rows = useMemo(() => ['left', 'right'].flatMap(side => deviceHistory[side].map((d, index) => ({
|
|
20
20
|
index, side,
|
|
21
21
|
brand: d.brand, device: d.device,
|
|
22
22
|
fitType: d.fitType ?? '', productName: d.productName ?? '',
|
|
@@ -7,23 +7,16 @@ import Table from '../Table';
|
|
|
7
7
|
import Button from '../Button';
|
|
8
8
|
import Checkbox from '../Checkbox';
|
|
9
9
|
import Badge from '../Badge';
|
|
10
|
-
import { deviceKindVariant } from '../../utils/deviceKind';
|
|
10
|
+
import { deviceKindVariant, deviceKindLabel } from '../../utils/deviceKind';
|
|
11
11
|
import { formatWornDate, wornDateSortKey } from '../../utils/wornDate';
|
|
12
|
-
/* 기기 종류 배지 이름은 지금 언어로 — 값은 저장된 한국어(보청기·인공와우)라 그대로 보이면 영어 화면에도 한국어였다.
|
|
13
|
-
색(deviceKindVariant)은 저장값으로 가른다. 모르는 값은 그대로 */
|
|
14
|
-
const 기기종류열쇠 = { '보청기': 'hearingAid', '인공와우': 'cochlearImplant' };
|
|
15
|
-
function 기기종류이름(device, t) {
|
|
16
|
-
const k = 기기종류열쇠[device];
|
|
17
|
-
return k ? t(`device.type.${k}`, { defaultValue: device }) : device;
|
|
18
|
-
}
|
|
19
12
|
import { useTablePaging, useTableSort } from '../../hooks/useTablePaging';
|
|
20
|
-
import {
|
|
13
|
+
import { compareCellBlankLast } from '../../utils/textSort';
|
|
21
14
|
import './DeviceListPopup.css';
|
|
22
15
|
/**
|
|
23
16
|
* 정렬에 쓸 문자열. 날짜는 **자리수를 채워** 잰다 — '2026.5.2' 와 '2026.11.5' 를
|
|
24
17
|
* 글자 그대로 견주면 5월이 11월보다 뒤로 간다.
|
|
25
18
|
*/
|
|
26
|
-
function
|
|
19
|
+
function sortValue(r, key) {
|
|
27
20
|
if (key !== 'wornAt')
|
|
28
21
|
return String(r[key] ?? '');
|
|
29
22
|
/* 월·일이 null 로 오는 옛 기록이 있다 — padStart 에서 표가 통째로 터졌다 */
|
|
@@ -31,36 +24,36 @@ function 정렬값(r, key) {
|
|
|
31
24
|
}
|
|
32
25
|
export default function DeviceSideTable({ rows, loading, onSelect, onDelete, onWear, onRelease, onAdd }) {
|
|
33
26
|
const { t, i18n } = useTranslation(); /* i18n.language — 착용일이 보는 말을 따른다((118)) */
|
|
34
|
-
const
|
|
27
|
+
const sideLabelOf = (side) => side === 'left' ? t('device.direction.left', '좌') : t('device.direction.right', '우');
|
|
35
28
|
/* 머리행을 누르면 그 칸으로 정렬한다 — 같은 칸을 다시 누르면 방향이 뒤집힌다.
|
|
36
29
|
쪽넘김보다 **앞에서** 정렬한다(훅에 이미 정렬된 줄을 건넨다). */
|
|
37
|
-
const {
|
|
30
|
+
const { sort, field } = useTableSort();
|
|
38
31
|
const sortedRows = useMemo(() => {
|
|
39
|
-
if (
|
|
32
|
+
if (!sort)
|
|
40
33
|
return rows;
|
|
41
|
-
const { key, asc } =
|
|
34
|
+
const { key, asc } = sort;
|
|
42
35
|
return [...rows].sort((a, b) => {
|
|
43
36
|
if (key === 'worn') {
|
|
44
37
|
const cmp = Number(b.worn) - Number(a.worn); // 켜진 것이 먼저
|
|
45
38
|
return asc ? cmp : -cmp;
|
|
46
39
|
}
|
|
47
|
-
return
|
|
40
|
+
return compareCellBlankLast(sortValue(a, key), sortValue(b, key), asc); // 빈 칸(착용일 없음 등)은 오름·내림 모두 맨 뒤 — useTableSort 와 같은 규칙
|
|
48
41
|
});
|
|
49
|
-
}, [rows,
|
|
42
|
+
}, [rows, sort]);
|
|
50
43
|
/* 개수 보기·쪽넘김은 방문 표와 같은 것을 쓴다 — 같은 자리에서 같은 뜻으로 읽히게 */
|
|
51
|
-
const { pageRows, pageSizeProps, paginationProps } = useTablePaging(sortedRows,
|
|
44
|
+
const { pageRows, pageSizeProps, paginationProps } = useTablePaging(sortedRows, sort);
|
|
52
45
|
return (_jsxs("div", { className: "dlp-side-table", children: [_jsx(Toolbar, { count: { value: rows.length, unit: t('customers.device.unit', '개') }, pageSize: pageSizeProps, pagination: paginationProps, actions: onAdd && (_jsx(Button, { htmlType: "button", size: 40, type: "bar", variant: "main", icon: "plus", label: t('customers.device.addButton', '기기 추가'), onClick: onAdd })) }), _jsx(Table, { loading: loading, resizable: true, resizeMode: "fit", resizeStorageKey: "customer-device-history-v2", fields: [
|
|
53
46
|
/* 맨 왼쪽 — 지금 끼고 있는 것만 좌/우를 적는다. 이 열만 훑으면 "오늘 이 사람이
|
|
54
47
|
무엇을 쓰고 있나" 가 한눈에 보인다(나머지 줄은 비어 있어 눈에 안 걸린다). */
|
|
55
48
|
/* 체크박스 칸은 폭을 늘릴 것이 없다 — 손잡이를 주면 잡히는데 아무 일도
|
|
56
49
|
일어나지 않는다. Table 이 이 경우를 위해 resizable:false 를 둔다. */
|
|
57
|
-
{
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
{ ...field(t('customers.device.wearingNow', '착용'), 'worn'), resizable: false },
|
|
51
|
+
field(t('device.direction.label', '방향'), 'side'),
|
|
52
|
+
field(t('device.deviceType', '종류'), 'device'),
|
|
53
|
+
field(t('device.brand', '브랜드'), 'brand'),
|
|
54
|
+
field(t('device.fitType', '착용형태'), 'fitType'),
|
|
55
|
+
field(t('device.productName', '제품명'), 'productName'),
|
|
56
|
+
field(t('device.wornDate', '착용일'), 'wornAt'),
|
|
64
57
|
/* 삭제 칸은 그 길을 준 화면에서만 — 머리행과 줄의 칸 수가 어긋나면 표가 밀린다 */
|
|
65
58
|
...(onDelete ? [{ label: '', resizable: false }] : []),
|
|
66
59
|
], items: pageRows.map((r, i) => ({ id: r.id ?? `${r.side}-${i}`, row: r })), gridTemplateColumns: `56px 110px 110px minmax(120px, 1fr) 100px minmax(120px, 1fr) 110px${onDelete ? ' 80px' : ''}`, renderRow: ({ row }) => [
|
|
@@ -79,11 +72,11 @@ export default function DeviceSideTable({ rows, loading, onSelect, onDelete, onW
|
|
|
79
72
|
onRelease?.(row); } }) })),
|
|
80
73
|
},
|
|
81
74
|
/* 방향 칸은 좌/우만 말한다 — 착용 여부는 맨 왼쪽 체크가 다룬다 */
|
|
82
|
-
{ content: _jsx("span", { children:
|
|
75
|
+
{ content: _jsx("span", { children: sideLabelOf(row.side) }) },
|
|
83
76
|
/* 종류는 위 패널의 종류 배지와 같은 모양으로 — 같은 값을 두 곳에서 다르게
|
|
84
77
|
그리면 같은 것인 줄 모른다. 값이 비면 배지도 세우지 않는다(빈 배지는
|
|
85
78
|
'무언가 있다'는 거짓 신호다). */
|
|
86
|
-
{ content: row.device ? _jsx(Badge, { size: "small", variant: deviceKindVariant(row.device), fill: "soft", label:
|
|
79
|
+
{ content: row.device ? _jsx(Badge, { size: "small", variant: deviceKindVariant(row.device), fill: "soft", label: deviceKindLabel(row.device, t) }) : _jsx("span", {}) },
|
|
87
80
|
{ content: _jsx("span", { children: row.brand }) },
|
|
88
81
|
{ content: _jsx("span", { children: row.fitType || '-' }) },
|
|
89
82
|
{ content: _jsx("span", { children: row.productName || '-' }) },
|