@ncds/ui-admin 1.8.14 → 1.8.16
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/dist/cjs/src/components/forms-and-input/checkbox/CheckboxInput.js +3 -2
- package/dist/cjs/src/components/forms-and-input/range-date-picker/RangeDatePicker.js +22 -0
- package/dist/cjs/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.js +95 -0
- package/dist/cjs/src/components/forms-and-input/range-date-picker-with-buttons/RangeDatePickerWithButtons.js +7 -1
- package/dist/cjs/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.js +57 -0
- package/dist/cjs/src/components/forms-and-input/slider/Slider.js +7 -1
- package/dist/cjs/src/components/navigation/pagination/Pagination.js +12 -2
- package/dist/cjs/src/components/navigation/pagination/__tests__/Pagination.test.js +62 -0
- package/dist/cjs/src/components/overlays/tooltip/Tooltip.js +15 -2
- package/dist/cjs/src/components/overlays/tooltip/__tests__/utils.test.js +40 -0
- package/dist/cjs/src/components/overlays/tooltip/utils.js +20 -0
- package/dist/cjs/src/hooks/index.js +11 -0
- package/dist/cjs/src/hooks/useIsomorphicLayoutEffect.js +8 -0
- package/dist/cjs/src/hooks/useMediaQuery.js +3 -0
- package/dist/esm/src/components/forms-and-input/checkbox/CheckboxInput.js +4 -3
- package/dist/esm/src/components/forms-and-input/range-date-picker/RangeDatePicker.js +22 -0
- package/dist/esm/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.js +92 -0
- package/dist/esm/src/components/forms-and-input/range-date-picker-with-buttons/RangeDatePickerWithButtons.js +7 -1
- package/dist/esm/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.js +54 -0
- package/dist/esm/src/components/forms-and-input/slider/Slider.js +7 -1
- package/dist/esm/src/components/navigation/pagination/Pagination.js +12 -2
- package/dist/esm/src/components/navigation/pagination/__tests__/Pagination.test.js +59 -0
- package/dist/esm/src/components/overlays/tooltip/Tooltip.js +15 -2
- package/dist/esm/src/components/overlays/tooltip/__tests__/utils.test.js +38 -0
- package/dist/esm/src/components/overlays/tooltip/utils.js +13 -0
- package/dist/esm/src/hooks/index.js +1 -0
- package/dist/esm/src/hooks/useIsomorphicLayoutEffect.js +2 -0
- package/dist/esm/src/hooks/useMediaQuery.js +3 -0
- package/dist/temp/src/components/forms-and-input/checkbox/CheckboxInput.js +4 -3
- package/dist/temp/src/components/forms-and-input/range-date-picker/RangeDatePicker.js +22 -0
- package/dist/temp/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.d.ts +1 -0
- package/dist/temp/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.js +60 -0
- package/dist/temp/src/components/forms-and-input/range-date-picker-with-buttons/RangeDatePickerWithButtons.js +9 -1
- package/dist/temp/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.d.ts +1 -0
- package/dist/temp/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.js +42 -0
- package/dist/temp/src/components/forms-and-input/slider/Slider.js +7 -1
- package/dist/temp/src/components/navigation/pagination/Pagination.js +12 -2
- package/dist/temp/src/components/navigation/pagination/__tests__/Pagination.test.d.ts +1 -0
- package/dist/temp/src/components/navigation/pagination/__tests__/Pagination.test.js +59 -0
- package/dist/temp/src/components/overlays/tooltip/Tooltip.js +21 -4
- package/dist/temp/src/components/overlays/tooltip/__tests__/utils.test.d.ts +1 -0
- package/dist/temp/src/components/overlays/tooltip/__tests__/utils.test.js +38 -0
- package/dist/temp/src/components/overlays/tooltip/utils.d.ts +9 -0
- package/dist/temp/src/components/overlays/tooltip/utils.js +13 -0
- package/dist/temp/src/hooks/index.d.ts +1 -0
- package/dist/temp/src/hooks/index.js +1 -0
- package/dist/temp/src/hooks/useIsomorphicLayoutEffect.d.ts +2 -0
- package/dist/temp/src/hooks/useIsomorphicLayoutEffect.js +2 -0
- package/dist/temp/src/hooks/useMediaQuery.js +3 -0
- package/dist/types/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.d.ts +1 -0
- package/dist/types/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.d.ts +1 -0
- package/dist/types/src/components/navigation/pagination/__tests__/Pagination.test.d.ts +1 -0
- package/dist/types/src/components/overlays/tooltip/__tests__/utils.test.d.ts +1 -0
- package/dist/types/src/components/overlays/tooltip/utils.d.ts +9 -0
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/useIsomorphicLayoutEffect.d.ts +2 -0
- package/dist/ui-admin/assets/styles/style.css +16 -20
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ exports.CheckboxInput = void 0;
|
|
|
7
7
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
8
8
|
var _classnames = _interopRequireDefault(require("classnames"));
|
|
9
9
|
var _react = require("react");
|
|
10
|
+
var _useIsomorphicLayoutEffect = require("../../../hooks/useIsomorphicLayoutEffect");
|
|
10
11
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
12
|
const CheckboxInput = exports.CheckboxInput = /*#__PURE__*/(0, _react.forwardRef)((_ref, ref) => {
|
|
12
13
|
let {
|
|
@@ -19,10 +20,10 @@ const CheckboxInput = exports.CheckboxInput = /*#__PURE__*/(0, _react.forwardRef
|
|
|
19
20
|
...props
|
|
20
21
|
} = _ref;
|
|
21
22
|
const inputRef = (0, _react.useRef)(null);
|
|
22
|
-
(0,
|
|
23
|
+
(0, _useIsomorphicLayoutEffect.useIsomorphicLayoutEffect)(() => {
|
|
23
24
|
const input = ref && typeof ref !== 'function' ? ref.current : inputRef.current;
|
|
24
25
|
if (input) {
|
|
25
|
-
input.indeterminate = indeterminate;
|
|
26
|
+
input.indeterminate = indeterminate;
|
|
26
27
|
}
|
|
27
28
|
}, [indeterminate, ref]);
|
|
28
29
|
return (0, _jsxRuntime.jsx)("span", {
|
|
@@ -10,6 +10,10 @@ var _react = require("react");
|
|
|
10
10
|
var _DatePicker = require("../date-picker/DatePicker");
|
|
11
11
|
var _utils = require("../date-picker/utils");
|
|
12
12
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
|
+
/** 시작일·종료일이 모두 채워져 있고 유효한 날짜일 때만 범위 검증이 의미가 있다.
|
|
14
|
+
* 한쪽이라도 비어 있으면(예: ENTIRE 초기화로 양쪽이 '') moment 비교가 전부 무효가 되어
|
|
15
|
+
* onDateValidation 에 "Invalid date" 같은 잘못된 newDate 가 전달되므로 검증을 건너뛴다. */
|
|
16
|
+
const areBothDatesValid = (start, end) => Boolean(start) && Boolean(end) && (0, _moment.default)(start).isValid() && (0, _moment.default)(end).isValid();
|
|
13
17
|
const RangeDatePicker = exports.RangeDatePicker = /*#__PURE__*/(0, _react.forwardRef)((_ref, ref) => {
|
|
14
18
|
let {
|
|
15
19
|
startDateOptions,
|
|
@@ -79,6 +83,9 @@ const RangeDatePicker = exports.RangeDatePicker = /*#__PURE__*/(0, _react.forwar
|
|
|
79
83
|
}
|
|
80
84
|
}, [startDateOptions, endDateOptions, validationOption, onDateValidation, getOverlapResult, getSettingNewDate]);
|
|
81
85
|
(0, _react.useEffect)(() => {
|
|
86
|
+
if (!areBothDatesValid(startDateOptions.currentDate, endDateOptions.currentDate)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
82
89
|
const isOverDate = (0, _moment.default)(startDateOptions.currentDate).isAfter(endDateOptions.currentDate);
|
|
83
90
|
if (isOverDate) {
|
|
84
91
|
changeSettingDateAndAlert('start');
|
|
@@ -102,8 +109,18 @@ const RangeDatePicker = exports.RangeDatePicker = /*#__PURE__*/(0, _react.forwar
|
|
|
102
109
|
} else {
|
|
103
110
|
changeSettingDateAndAlert('start', true);
|
|
104
111
|
}
|
|
112
|
+
// 시작일이 변경될 때만 검증한다. 반대편 날짜·콜백·setting 을 deps 에 넣으면, 부모가 매 렌더
|
|
113
|
+
// 새 options 객체와 onDateValidation 을 내려보내는 경우(예: RangeDatePickerWithButtons)
|
|
114
|
+
// effect 가 매 렌더 재실행되어 검증 루프가 발생하므로 의도적으로 deps 를 좁게 유지한다.
|
|
115
|
+
// (과거 무한 루프 수정 47f09427 + deps 축소 1c5ac7ad 의 결과 — deps 를 되돌리면 루프 재발)
|
|
116
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
105
117
|
}, [startDateOptions.currentDate]);
|
|
106
118
|
(0, _react.useEffect)(() => {
|
|
119
|
+
if (!areBothDatesValid(startDateOptions.currentDate, endDateOptions.currentDate)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// 종료일이 '오늘'이면 종료측 검증을 건너뛴다: shopby 는 종료일=오늘이 정상 기본값이라
|
|
123
|
+
// (고도몰=어제) 이 상태를 에러로 잡지 않기 위함. (도입 d1bd8751)
|
|
107
124
|
const isNotTodayEndDate = !(0, _moment.default)(endDateOptions.currentDate).isSame((0, _moment.default)(), 'day');
|
|
108
125
|
if (!validationOption?.setting || !isNotTodayEndDate) {
|
|
109
126
|
return;
|
|
@@ -128,6 +145,11 @@ const RangeDatePicker = exports.RangeDatePicker = /*#__PURE__*/(0, _react.forwar
|
|
|
128
145
|
} else {
|
|
129
146
|
changeSettingDateAndAlert('end', true);
|
|
130
147
|
}
|
|
148
|
+
// 종료일이 변경될 때만 검증한다(위 시작일 effect 와 동일 — 47f09427/1c5ac7ad).
|
|
149
|
+
// 본문이 startDateOptions.currentDate 도 참조하지만 시작일 변경 시 종료측 검증을 다시 돌리지
|
|
150
|
+
// 않는 건 의도된 것이다: 시작일 변경은 위 effect 가 type:'start' 로 처리하며, 여기에 start 를
|
|
151
|
+
// deps 로 넣으면 1c5ac7ad 가 제거한 그 루프가 재발한다.
|
|
152
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
131
153
|
}, [endDateOptions.currentDate]);
|
|
132
154
|
return (0, _jsxRuntime.jsxs)("div", {
|
|
133
155
|
className: "ncua-range-date-picker",
|
package/dist/cjs/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _react = require("react");
|
|
4
|
+
var _client = require("react-dom/client");
|
|
5
|
+
var _testUtils = require("react-dom/test-utils");
|
|
6
|
+
var _vitest = require("vitest");
|
|
7
|
+
var _RangeDatePicker = require("../RangeDatePicker");
|
|
8
|
+
// @vitest-environment jsdom
|
|
9
|
+
|
|
10
|
+
const mounted = [];
|
|
11
|
+
function mount(props) {
|
|
12
|
+
const container = document.createElement('div');
|
|
13
|
+
document.body.appendChild(container);
|
|
14
|
+
const root = (0, _client.createRoot)(container);
|
|
15
|
+
(0, _testUtils.act)(() => {
|
|
16
|
+
root.render(/*#__PURE__*/(0, _react.createElement)(_RangeDatePicker.RangeDatePicker, props));
|
|
17
|
+
});
|
|
18
|
+
mounted.push({
|
|
19
|
+
root,
|
|
20
|
+
container
|
|
21
|
+
});
|
|
22
|
+
return container;
|
|
23
|
+
}
|
|
24
|
+
(0, _vitest.afterEach)(() => {
|
|
25
|
+
for (const {
|
|
26
|
+
root,
|
|
27
|
+
container
|
|
28
|
+
} of mounted.splice(0)) {
|
|
29
|
+
(0, _testUtils.act)(() => {
|
|
30
|
+
root.unmount();
|
|
31
|
+
});
|
|
32
|
+
container.remove();
|
|
33
|
+
}
|
|
34
|
+
_vitest.vi.restoreAllMocks();
|
|
35
|
+
});
|
|
36
|
+
(0, _vitest.describe)('#1 빈/무효 날짜 검증 가드', () => {
|
|
37
|
+
const validationOption = {
|
|
38
|
+
setting: {
|
|
39
|
+
unit: 'days',
|
|
40
|
+
period: 7
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
(0, _vitest.it)('양쪽 날짜가 비어 있으면 onDateValidation 을 호출하지 않는다', () => {
|
|
44
|
+
const onDateValidation = _vitest.vi.fn();
|
|
45
|
+
mount({
|
|
46
|
+
startDateOptions: {
|
|
47
|
+
currentDate: '',
|
|
48
|
+
onChangeDate: () => undefined
|
|
49
|
+
},
|
|
50
|
+
endDateOptions: {
|
|
51
|
+
currentDate: '',
|
|
52
|
+
onChangeDate: () => undefined
|
|
53
|
+
},
|
|
54
|
+
validationOption,
|
|
55
|
+
onDateValidation
|
|
56
|
+
});
|
|
57
|
+
(0, _vitest.expect)(onDateValidation).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
(0, _vitest.it)('한쪽(종료일)만 비어 있어도 "Invalid date" 를 newDate 로 전달하지 않는다', () => {
|
|
60
|
+
const onDateValidation = _vitest.vi.fn();
|
|
61
|
+
mount({
|
|
62
|
+
startDateOptions: {
|
|
63
|
+
currentDate: '2024-03-01',
|
|
64
|
+
onChangeDate: () => undefined
|
|
65
|
+
},
|
|
66
|
+
endDateOptions: {
|
|
67
|
+
currentDate: '',
|
|
68
|
+
onChangeDate: () => undefined
|
|
69
|
+
},
|
|
70
|
+
validationOption,
|
|
71
|
+
onDateValidation
|
|
72
|
+
});
|
|
73
|
+
const leakedInvalidDate = onDateValidation.mock.calls.some(_ref => {
|
|
74
|
+
let [arg] = _ref;
|
|
75
|
+
return arg?.newDate === 'Invalid date';
|
|
76
|
+
});
|
|
77
|
+
(0, _vitest.expect)(leakedInvalidDate).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
(0, _vitest.it)('양쪽 날짜가 유효하면 검증이 정상 동작한다(가드가 정상 입력을 막지 않음)', () => {
|
|
80
|
+
const onDateValidation = _vitest.vi.fn();
|
|
81
|
+
mount({
|
|
82
|
+
startDateOptions: {
|
|
83
|
+
currentDate: '2024-03-01',
|
|
84
|
+
onChangeDate: () => undefined
|
|
85
|
+
},
|
|
86
|
+
endDateOptions: {
|
|
87
|
+
currentDate: '2024-03-05',
|
|
88
|
+
onChangeDate: () => undefined
|
|
89
|
+
},
|
|
90
|
+
validationOption,
|
|
91
|
+
onDateValidation
|
|
92
|
+
});
|
|
93
|
+
(0, _vitest.expect)(onDateValidation).toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -39,6 +39,9 @@ const RangeDatePickerWithButtons = _ref => {
|
|
|
39
39
|
unit: currentPeriodItem.unit
|
|
40
40
|
}));
|
|
41
41
|
startDateOptions.onChangeDate(startDate);
|
|
42
|
+
// 종료일 기준이 플랫폼마다 다르다: 고도몰=어제, shopby=오늘.
|
|
43
|
+
// useYesterdayAsEndDate=true 면 종료일을 어제로(고도몰), false 면 오늘로(shopby) 둔다.
|
|
44
|
+
// 종료일만 옮길 뿐 시작일을 종료일 기준으로 재계산하지는 않는다(abc16b3d).
|
|
42
45
|
const endDate = useYesterdayAsEndDate ? (0, _datePicker2.getDateFormat)((0, _datePicker2.getSubtractDate)({
|
|
43
46
|
period: 1,
|
|
44
47
|
unit: 'days'
|
|
@@ -53,12 +56,15 @@ const RangeDatePickerWithButtons = _ref => {
|
|
|
53
56
|
if (currentButtonId !== 'NONE') {
|
|
54
57
|
setCalculatedDate();
|
|
55
58
|
}
|
|
59
|
+
// 선택된 기간 버튼(currentButtonId)이 바뀔 때만 날짜를 재계산한다. setCalculatedDate 는
|
|
60
|
+
// 매 렌더 새로 생성되므로 deps 에 넣으면 effect 가 매 렌더 실행된다 — 의도적으로 좁게 유지.
|
|
61
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
56
62
|
}, [currentButtonId]);
|
|
57
63
|
return (0, _jsxRuntime.jsxs)("div", {
|
|
58
64
|
className: "ncua-range-date-picker-with-buttons",
|
|
59
65
|
children: [(0, _jsxRuntime.jsx)(_buttonGroup.ButtonGroup, {
|
|
60
66
|
size: size,
|
|
61
|
-
children: periodKeys.map(key => (0, _jsxRuntime.jsx)(_buttonGroup.ButtonGroup.Item, {
|
|
67
|
+
children: periodKeys.filter(key => items[key]).map(key => (0, _jsxRuntime.jsx)(_buttonGroup.ButtonGroup.Item, {
|
|
62
68
|
isCurrent: currentButtonId === key,
|
|
63
69
|
label: items[key].text,
|
|
64
70
|
onClick: () => setCurrentButtonId(key)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _react = require("react");
|
|
4
|
+
var _client = require("react-dom/client");
|
|
5
|
+
var _testUtils = require("react-dom/test-utils");
|
|
6
|
+
var _vitest = require("vitest");
|
|
7
|
+
var _RangeDatePickerWithButtons = require("../RangeDatePickerWithButtons");
|
|
8
|
+
// @vitest-environment jsdom
|
|
9
|
+
|
|
10
|
+
const mounted = [];
|
|
11
|
+
function mount(props) {
|
|
12
|
+
const container = document.createElement('div');
|
|
13
|
+
document.body.appendChild(container);
|
|
14
|
+
const root = (0, _client.createRoot)(container);
|
|
15
|
+
(0, _testUtils.act)(() => {
|
|
16
|
+
root.render(/*#__PURE__*/(0, _react.createElement)(_RangeDatePickerWithButtons.RangeDatePickerWithButtons, props));
|
|
17
|
+
});
|
|
18
|
+
mounted.push({
|
|
19
|
+
root,
|
|
20
|
+
container
|
|
21
|
+
});
|
|
22
|
+
return container;
|
|
23
|
+
}
|
|
24
|
+
(0, _vitest.afterEach)(() => {
|
|
25
|
+
for (const {
|
|
26
|
+
root,
|
|
27
|
+
container
|
|
28
|
+
} of mounted.splice(0)) {
|
|
29
|
+
(0, _testUtils.act)(() => {
|
|
30
|
+
root.unmount();
|
|
31
|
+
});
|
|
32
|
+
container.remove();
|
|
33
|
+
}
|
|
34
|
+
_vitest.vi.restoreAllMocks();
|
|
35
|
+
});
|
|
36
|
+
(0, _vitest.describe)('#6 periodItems 에 없는 periodKey 방어', () => {
|
|
37
|
+
const baseProps = {
|
|
38
|
+
currentButtonId: 'NONE',
|
|
39
|
+
setCurrentButtonId: () => undefined,
|
|
40
|
+
startDateOptions: {
|
|
41
|
+
currentDate: '2024-03-01',
|
|
42
|
+
onChangeDate: () => undefined
|
|
43
|
+
},
|
|
44
|
+
endDateOptions: {
|
|
45
|
+
currentDate: '2024-03-05',
|
|
46
|
+
onChangeDate: () => undefined
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
(0, _vitest.it)('periodKeys 에 periodItems 에 없는 키가 있어도 크래시하지 않고 유효한 버튼만 렌더한다', () => {
|
|
50
|
+
const container = mount({
|
|
51
|
+
...baseProps,
|
|
52
|
+
periodKeys: ['TODAY', 'UNKNOWN_KEY']
|
|
53
|
+
});
|
|
54
|
+
// 유효한 키(TODAY='오늘')는 렌더되고, 정의에 없는 키는 건너뛴다(크래시 없음)
|
|
55
|
+
(0, _vitest.expect)(container.textContent).toContain('오늘');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -20,6 +20,9 @@ const Slider = _ref => {
|
|
|
20
20
|
} = _ref;
|
|
21
21
|
const containerRef = (0, _react.useRef)(null);
|
|
22
22
|
const sliderInstanceRef = (0, _react.useRef)(null);
|
|
23
|
+
// value 의 "타입"이 바뀔 때만(범위↔단일 등) 슬라이더를 재생성한다. valueType 으로 추출해
|
|
24
|
+
// 정적으로 드러낸다 — 96cf7b3a 에서 deps 를 [] → [typeof value, ...] 로 좁힌 의도.
|
|
25
|
+
const valueType = typeof value;
|
|
23
26
|
(0, _react.useEffect)(() => {
|
|
24
27
|
if (!containerRef.current) return;
|
|
25
28
|
sliderInstanceRef.current = new _slider.Slider(containerRef.current, {
|
|
@@ -37,7 +40,10 @@ const Slider = _ref => {
|
|
|
37
40
|
sliderInstanceRef.current = null;
|
|
38
41
|
}
|
|
39
42
|
};
|
|
40
|
-
|
|
43
|
+
// value 변경은 아래 setValue effect, disabled 변경은 enable/disable effect 가 처리하므로
|
|
44
|
+
// 여기 deps 에 넣지 않는다(넣으면 매 렌더 인스턴스 재생성 → 상태 손실).
|
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
+
}, [valueType, onChange, min, max, step, labelPosition]);
|
|
41
47
|
(0, _react.useEffect)(() => {
|
|
42
48
|
if (sliderInstanceRef.current) {
|
|
43
49
|
sliderInstanceRef.current.setValue(value);
|
|
@@ -33,6 +33,13 @@ const Pagination = _ref => {
|
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
35
|
(0, _react.useEffect)(() => {
|
|
36
|
+
const lastGroupStart = Math.max(1, Math.floor((totalPage - 1) / pageCount) * pageCount + 1);
|
|
37
|
+
// totalPage/pageCount 가 줄어 start 가 마지막 그룹을 넘으면 마지막 그룹으로 보정한다.
|
|
38
|
+
// (예: 100→50 페이지로 축소 시 start 가 91 에 머물러 범위 밖 버튼을 표시하던 문제)
|
|
39
|
+
if (start > lastGroupStart) {
|
|
40
|
+
setStart(lastGroupStart);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
36
43
|
if (currentPage === start + pageCount) {
|
|
37
44
|
setStart(start + pageCount);
|
|
38
45
|
}
|
|
@@ -47,10 +54,13 @@ const Pagination = _ref => {
|
|
|
47
54
|
setStart(1);
|
|
48
55
|
}
|
|
49
56
|
if (currentPage === totalPage) {
|
|
50
|
-
const lastGroupStart = Math.floor((totalPage - 1) / pageCount) * pageCount + 1;
|
|
51
57
|
setStart(lastGroupStart);
|
|
52
58
|
}
|
|
53
|
-
|
|
59
|
+
// start 는 이 effect 가 setStart 로 갱신하는 대상이라 deps 에서 제외한다(넣으면 자기 갱신으로
|
|
60
|
+
// 재실행). currentPage 외에 totalPage/pageCount 도 추적해, 이들이 줄어 start 가 범위를 벗어나면
|
|
61
|
+
// 위에서 보정한다.
|
|
62
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
63
|
+
}, [currentPage, totalPage, pageCount]);
|
|
54
64
|
return (0, _jsxRuntime.jsxs)("div", {
|
|
55
65
|
className: (0, _classnames.default)('ncua-pagination', `ncua-pagination--${breakPoint}`, className),
|
|
56
66
|
children: [showJumpPageButton && (0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _react = require("react");
|
|
4
|
+
var _client = require("react-dom/client");
|
|
5
|
+
var _testUtils = require("react-dom/test-utils");
|
|
6
|
+
var _vitest = require("vitest");
|
|
7
|
+
var _Pagination = require("../Pagination");
|
|
8
|
+
// @vitest-environment jsdom
|
|
9
|
+
|
|
10
|
+
const ITEM_COUNT_PER_PAGE = 10;
|
|
11
|
+
const PAGE_GROUP_SIZE = 10;
|
|
12
|
+
const TOTAL_COUNT_100_PAGES = 1000;
|
|
13
|
+
const TOTAL_COUNT_50_PAGES = 500;
|
|
14
|
+
const CURRENT_PAGE = 91; // 100페이지 기준 마지막 그룹(91~100)을 보던 페이지
|
|
15
|
+
const GROUP_START_100_PAGES = 91; // currentPage=91 일 때의 그룹 시작
|
|
16
|
+
const GROUP_START_50_PAGES = 41; // totalPage=50 으로 줄었을 때의 마지막 그룹 시작
|
|
17
|
+
const LAST_PAGE_50 = 50;
|
|
18
|
+
let root = null;
|
|
19
|
+
let container = null;
|
|
20
|
+
function render(props) {
|
|
21
|
+
if (!container) {
|
|
22
|
+
container = document.createElement('div');
|
|
23
|
+
document.body.appendChild(container);
|
|
24
|
+
root = (0, _client.createRoot)(container);
|
|
25
|
+
}
|
|
26
|
+
(0, _testUtils.act)(() => {
|
|
27
|
+
root?.render(/*#__PURE__*/(0, _react.createElement)(_Pagination.Pagination, props));
|
|
28
|
+
});
|
|
29
|
+
return container;
|
|
30
|
+
}
|
|
31
|
+
(0, _vitest.afterEach)(() => {
|
|
32
|
+
if (root && container) {
|
|
33
|
+
(0, _testUtils.act)(() => root?.unmount());
|
|
34
|
+
container.remove();
|
|
35
|
+
}
|
|
36
|
+
root = null;
|
|
37
|
+
container = null;
|
|
38
|
+
});
|
|
39
|
+
const pageNums = el => Array.from(el.querySelectorAll('.ncua-pagination__page-num')).map(b => Number(b.textContent));
|
|
40
|
+
(0, _vitest.describe)('#332 Pagination — totalPage 축소 시 start 보정', () => {
|
|
41
|
+
(0, _vitest.it)('totalPage 가 줄면 start 를 마지막 그룹으로 보정하고 범위 밖 페이지 버튼을 표시하지 않는다', () => {
|
|
42
|
+
// 1) 100 페이지에서 91 페이지 보는 중 → 91~100 그룹 표시
|
|
43
|
+
const el = render({
|
|
44
|
+
totalCount: TOTAL_COUNT_100_PAGES,
|
|
45
|
+
itemCountPerPage: ITEM_COUNT_PER_PAGE,
|
|
46
|
+
pageCount: PAGE_GROUP_SIZE,
|
|
47
|
+
currentPage: CURRENT_PAGE
|
|
48
|
+
});
|
|
49
|
+
(0, _vitest.expect)(pageNums(el)[0]).toBe(GROUP_START_100_PAGES);
|
|
50
|
+
// 2) 데이터가 50 페이지로 줄어듦 (currentPage 는 그대로 — 부모가 clamp 안 한 상황)
|
|
51
|
+
render({
|
|
52
|
+
totalCount: TOTAL_COUNT_50_PAGES,
|
|
53
|
+
itemCountPerPage: ITEM_COUNT_PER_PAGE,
|
|
54
|
+
pageCount: PAGE_GROUP_SIZE,
|
|
55
|
+
currentPage: CURRENT_PAGE
|
|
56
|
+
});
|
|
57
|
+
const nums = pageNums(el);
|
|
58
|
+
// 마지막 그룹(41~50)으로 보정되고, totalPage(50)를 넘는 버튼은 없어야 한다
|
|
59
|
+
(0, _vitest.expect)(nums[0]).toBe(GROUP_START_50_PAGES);
|
|
60
|
+
(0, _vitest.expect)(Math.max(...nums)).toBeLessThanOrEqual(LAST_PAGE_50);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -10,6 +10,7 @@ var _classnames = _interopRequireDefault(require("classnames"));
|
|
|
10
10
|
var _react = require("react");
|
|
11
11
|
var _reactDom = require("react-dom");
|
|
12
12
|
var _ButtonCloseX = require("../../action/button/ButtonCloseX");
|
|
13
|
+
var _utils = require("./utils");
|
|
13
14
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
14
15
|
const ICON_SIZE_SM = 14;
|
|
15
16
|
const ICON_SIZE_DEFAULT = 16;
|
|
@@ -71,9 +72,16 @@ const computePanelCoords = (prefer, anchor, panel) => {
|
|
|
71
72
|
}
|
|
72
73
|
left = Math.max(VIEWPORT_MARGIN, Math.min(left, window.innerWidth - panel.width - VIEWPORT_MARGIN));
|
|
73
74
|
top = Math.max(VIEWPORT_MARGIN, Math.min(top, window.innerHeight - panel.height - VIEWPORT_MARGIN));
|
|
75
|
+
// 경계 보정으로 패널이 앵커 중심에서 밀렸을 때도 화살표(::after)가 앵커를 가리키도록
|
|
76
|
+
// 패널 내 X 오프셋 계산 (top/bottom 계열만 — SCSS의 --top/--bottom 규칙이 소비)
|
|
77
|
+
let arrowX = null;
|
|
78
|
+
if (calculatedPosition === 'top' || calculatedPosition === 'bottom') {
|
|
79
|
+
arrowX = (0, _utils.calculateArrowOffsetX)(anchor.left + anchor.width / 2, left, panel.width);
|
|
80
|
+
}
|
|
74
81
|
return {
|
|
75
82
|
top: Math.round(top),
|
|
76
83
|
left: Math.round(left),
|
|
84
|
+
arrowX,
|
|
77
85
|
calculatedPosition
|
|
78
86
|
};
|
|
79
87
|
};
|
|
@@ -86,6 +94,9 @@ const buildPanelStyle = (disablePortal, coords, visible, zIndex) => {
|
|
|
86
94
|
top: `${coords.top}px`,
|
|
87
95
|
left: `${coords.left}px`,
|
|
88
96
|
opacity,
|
|
97
|
+
...(coords.arrowX !== null && {
|
|
98
|
+
'--ncua-tooltip-arrow-x': `${coords.arrowX}px`
|
|
99
|
+
}),
|
|
89
100
|
...(zIndex && {
|
|
90
101
|
zIndex
|
|
91
102
|
})
|
|
@@ -138,7 +149,8 @@ const Tooltip = _ref => {
|
|
|
138
149
|
const [mounted, setMounted] = (0, _react.useState)(false);
|
|
139
150
|
const [coords, setCoords] = (0, _react.useState)({
|
|
140
151
|
top: OFFSCREEN,
|
|
141
|
-
left: OFFSCREEN
|
|
152
|
+
left: OFFSCREEN,
|
|
153
|
+
arrowX: null
|
|
142
154
|
});
|
|
143
155
|
const [calculatedPosition, setCalculatedPosition] = (0, _react.useState)(position === 'auto' ? 'bottom' : position);
|
|
144
156
|
const [isVisible, setIsVisible] = (0, _react.useState)(false);
|
|
@@ -160,7 +172,8 @@ const Tooltip = _ref => {
|
|
|
160
172
|
const next = computePanelCoords(position, anchor, panel);
|
|
161
173
|
setCoords({
|
|
162
174
|
top: next.top,
|
|
163
|
-
left: next.left
|
|
175
|
+
left: next.left,
|
|
176
|
+
arrowX: next.arrowX
|
|
164
177
|
});
|
|
165
178
|
setCalculatedPosition(next.calculatedPosition);
|
|
166
179
|
}, [position, disablePortal]);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _vitest = require("vitest");
|
|
4
|
+
var _utils = require("../utils");
|
|
5
|
+
// 공통 픽스처 — 너비 300px 패널이 viewport 왼쪽 여백(8px)에 클램핑된 상태를 가정
|
|
6
|
+
const PANEL_WIDTH = 300;
|
|
7
|
+
const CLAMPED_LEFT = 8;
|
|
8
|
+
(0, _vitest.describe)('calculateArrowOffsetX — 화살표가 앵커 중심을 가리키도록 패널 내 X 오프셋 계산', () => {
|
|
9
|
+
(0, _vitest.it)('패널이 클램핑되지 않으면(앵커 중심 = 패널 중심) 패널 너비의 절반을 반환한다', () => {
|
|
10
|
+
const anchorCenterX = 200;
|
|
11
|
+
const centeredPanelLeft = 50;
|
|
12
|
+
const panelHalfWidth = 150;
|
|
13
|
+
(0, _vitest.expect)((0, _utils.calculateArrowOffsetX)(anchorCenterX, centeredPanelLeft, PANEL_WIDTH)).toBe(panelHalfWidth);
|
|
14
|
+
});
|
|
15
|
+
(0, _vitest.it)('패널이 오른쪽으로 밀린 경우(왼쪽 경계 클램핑) 화살표는 왼쪽으로 이동해 앵커를 가리킨다', () => {
|
|
16
|
+
const anchorCenterX = 60;
|
|
17
|
+
const expectedOffset = 52; // 60 - 8
|
|
18
|
+
(0, _vitest.expect)((0, _utils.calculateArrowOffsetX)(anchorCenterX, CLAMPED_LEFT, PANEL_WIDTH)).toBe(expectedOffset);
|
|
19
|
+
});
|
|
20
|
+
(0, _vitest.it)('패널이 왼쪽으로 밀린 경우(오른쪽 경계 클램핑) 화살표는 오른쪽으로 이동해 앵커를 가리킨다', () => {
|
|
21
|
+
const anchorCenterX = 950;
|
|
22
|
+
const clampedRightPanelLeft = 692;
|
|
23
|
+
const expectedOffset = 258; // 950 - 692
|
|
24
|
+
(0, _vitest.expect)((0, _utils.calculateArrowOffsetX)(anchorCenterX, clampedRightPanelLeft, PANEL_WIDTH)).toBe(expectedOffset);
|
|
25
|
+
});
|
|
26
|
+
(0, _vitest.it)('앵커가 패널 왼쪽 모서리보다 바깥이면 최소 여백(모서리 radius + 화살표)으로 클램핑한다', () => {
|
|
27
|
+
const outsideLeftAnchorX = 10;
|
|
28
|
+
(0, _vitest.expect)((0, _utils.calculateArrowOffsetX)(outsideLeftAnchorX, CLAMPED_LEFT, PANEL_WIDTH)).toBe(_utils.ARROW_EDGE_MARGIN);
|
|
29
|
+
});
|
|
30
|
+
(0, _vitest.it)('앵커가 패널 오른쪽 모서리보다 바깥이면 최대 여백으로 클램핑한다', () => {
|
|
31
|
+
const outsideRightAnchorX = 999;
|
|
32
|
+
(0, _vitest.expect)((0, _utils.calculateArrowOffsetX)(outsideRightAnchorX, CLAMPED_LEFT, PANEL_WIDTH)).toBe(PANEL_WIDTH - _utils.ARROW_EDGE_MARGIN);
|
|
33
|
+
});
|
|
34
|
+
(0, _vitest.it)('소수 좌표는 반올림한다', () => {
|
|
35
|
+
const fractionalAnchorX = 100.4;
|
|
36
|
+
const panelLeft = 50;
|
|
37
|
+
const expectedRounded = 50; // 100.4 - 50 = 50.4 → 50
|
|
38
|
+
(0, _vitest.expect)((0, _utils.calculateArrowOffsetX)(fractionalAnchorX, panelLeft, PANEL_WIDTH)).toBe(expectedRounded);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.ARROW_EDGE_MARGIN = void 0;
|
|
7
|
+
exports.calculateArrowOffsetX = calculateArrowOffsetX;
|
|
8
|
+
// 화살표가 패널 모서리(border-radius 6px + 화살표 반너비 6px) 안쪽으로 파고들지 않기 위한 최소 여백
|
|
9
|
+
const ARROW_EDGE_MARGIN = exports.ARROW_EDGE_MARGIN = 12;
|
|
10
|
+
/**
|
|
11
|
+
* top/bottom 툴팁 화살표(::after)의 패널 내 X 오프셋.
|
|
12
|
+
* 패널이 viewport 경계 보정으로 앵커 중심에서 밀려도 화살표는 앵커를 가리키도록,
|
|
13
|
+
* 클램핑된 패널 left 기준으로 앵커 중심까지의 거리를 계산한다.
|
|
14
|
+
* 앵커가 패널 밖에 있으면 패널 모서리 여백까지만 이동한다.
|
|
15
|
+
* React(Tooltip.tsx)와 HTML 구현체(assets/scripts/tooltip)가 공유한다.
|
|
16
|
+
*/
|
|
17
|
+
function calculateArrowOffsetX(anchorCenterX, panelLeft, panelWidth) {
|
|
18
|
+
const raw = anchorCenterX - panelLeft;
|
|
19
|
+
return Math.round(Math.max(ARROW_EDGE_MARGIN, Math.min(raw, panelWidth - ARROW_EDGE_MARGIN)));
|
|
20
|
+
}
|
|
@@ -36,6 +36,17 @@ Object.keys(_useFloatingPosition).forEach(function (key) {
|
|
|
36
36
|
}
|
|
37
37
|
});
|
|
38
38
|
});
|
|
39
|
+
var _useIsomorphicLayoutEffect = require("./useIsomorphicLayoutEffect");
|
|
40
|
+
Object.keys(_useIsomorphicLayoutEffect).forEach(function (key) {
|
|
41
|
+
if (key === "default" || key === "__esModule") return;
|
|
42
|
+
if (key in exports && exports[key] === _useIsomorphicLayoutEffect[key]) return;
|
|
43
|
+
Object.defineProperty(exports, key, {
|
|
44
|
+
enumerable: true,
|
|
45
|
+
get: function () {
|
|
46
|
+
return _useIsomorphicLayoutEffect[key];
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
39
50
|
var _useMediaQuery = require("./useMediaQuery");
|
|
40
51
|
Object.keys(_useMediaQuery).forEach(function (key) {
|
|
41
52
|
if (key === "default" || key === "__esModule") return;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useIsomorphicLayoutEffect = void 0;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
const useIsomorphicLayoutEffect = exports.useIsomorphicLayoutEffect = typeof window !== 'undefined' ? _react.useLayoutEffect : _react.useEffect;
|
|
@@ -20,6 +20,9 @@ const useMediaQuery = (query, options) => {
|
|
|
20
20
|
return () => {
|
|
21
21
|
mediaQuery.removeEventListener('change', handleChange);
|
|
22
22
|
};
|
|
23
|
+
// options 객체 전체가 아니라 실제 사용하는 onMatched 콜백에만 의존한다. options 를 통째로
|
|
24
|
+
// 넣으면 매 렌더 새 객체로 간주돼 불필요하게 재구독한다.
|
|
25
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
23
26
|
}, [query, options?.onMatched]);
|
|
24
27
|
return matches;
|
|
25
28
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import classNames from 'classnames';
|
|
3
|
-
import { forwardRef,
|
|
3
|
+
import { forwardRef, useRef } from 'react';
|
|
4
|
+
import { useIsomorphicLayoutEffect } from '../../../hooks/useIsomorphicLayoutEffect';
|
|
4
5
|
export const CheckboxInput = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
5
6
|
let {
|
|
6
7
|
size = 'xs',
|
|
@@ -12,10 +13,10 @@ export const CheckboxInput = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
12
13
|
...props
|
|
13
14
|
} = _ref;
|
|
14
15
|
const inputRef = useRef(null);
|
|
15
|
-
|
|
16
|
+
useIsomorphicLayoutEffect(() => {
|
|
16
17
|
const input = ref && typeof ref !== 'function' ? ref.current : inputRef.current;
|
|
17
18
|
if (input) {
|
|
18
|
-
input.indeterminate = indeterminate;
|
|
19
|
+
input.indeterminate = indeterminate;
|
|
19
20
|
}
|
|
20
21
|
}, [indeterminate, ref]);
|
|
21
22
|
return _jsx("span", {
|
|
@@ -3,6 +3,10 @@ import moment from 'moment';
|
|
|
3
3
|
import { forwardRef, useCallback, useEffect } from 'react';
|
|
4
4
|
import { DatePicker } from '../date-picker/DatePicker';
|
|
5
5
|
import { getDateFormat } from '../date-picker/utils';
|
|
6
|
+
/** 시작일·종료일이 모두 채워져 있고 유효한 날짜일 때만 범위 검증이 의미가 있다.
|
|
7
|
+
* 한쪽이라도 비어 있으면(예: ENTIRE 초기화로 양쪽이 '') moment 비교가 전부 무효가 되어
|
|
8
|
+
* onDateValidation 에 "Invalid date" 같은 잘못된 newDate 가 전달되므로 검증을 건너뛴다. */
|
|
9
|
+
const areBothDatesValid = (start, end) => Boolean(start) && Boolean(end) && moment(start).isValid() && moment(end).isValid();
|
|
6
10
|
const RangeDatePicker = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
7
11
|
let {
|
|
8
12
|
startDateOptions,
|
|
@@ -72,6 +76,9 @@ const RangeDatePicker = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
72
76
|
}
|
|
73
77
|
}, [startDateOptions, endDateOptions, validationOption, onDateValidation, getOverlapResult, getSettingNewDate]);
|
|
74
78
|
useEffect(() => {
|
|
79
|
+
if (!areBothDatesValid(startDateOptions.currentDate, endDateOptions.currentDate)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
75
82
|
const isOverDate = moment(startDateOptions.currentDate).isAfter(endDateOptions.currentDate);
|
|
76
83
|
if (isOverDate) {
|
|
77
84
|
changeSettingDateAndAlert('start');
|
|
@@ -95,8 +102,18 @@ const RangeDatePicker = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
95
102
|
} else {
|
|
96
103
|
changeSettingDateAndAlert('start', true);
|
|
97
104
|
}
|
|
105
|
+
// 시작일이 변경될 때만 검증한다. 반대편 날짜·콜백·setting 을 deps 에 넣으면, 부모가 매 렌더
|
|
106
|
+
// 새 options 객체와 onDateValidation 을 내려보내는 경우(예: RangeDatePickerWithButtons)
|
|
107
|
+
// effect 가 매 렌더 재실행되어 검증 루프가 발생하므로 의도적으로 deps 를 좁게 유지한다.
|
|
108
|
+
// (과거 무한 루프 수정 47f09427 + deps 축소 1c5ac7ad 의 결과 — deps 를 되돌리면 루프 재발)
|
|
109
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
98
110
|
}, [startDateOptions.currentDate]);
|
|
99
111
|
useEffect(() => {
|
|
112
|
+
if (!areBothDatesValid(startDateOptions.currentDate, endDateOptions.currentDate)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// 종료일이 '오늘'이면 종료측 검증을 건너뛴다: shopby 는 종료일=오늘이 정상 기본값이라
|
|
116
|
+
// (고도몰=어제) 이 상태를 에러로 잡지 않기 위함. (도입 d1bd8751)
|
|
100
117
|
const isNotTodayEndDate = !moment(endDateOptions.currentDate).isSame(moment(), 'day');
|
|
101
118
|
if (!validationOption?.setting || !isNotTodayEndDate) {
|
|
102
119
|
return;
|
|
@@ -121,6 +138,11 @@ const RangeDatePicker = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
121
138
|
} else {
|
|
122
139
|
changeSettingDateAndAlert('end', true);
|
|
123
140
|
}
|
|
141
|
+
// 종료일이 변경될 때만 검증한다(위 시작일 effect 와 동일 — 47f09427/1c5ac7ad).
|
|
142
|
+
// 본문이 startDateOptions.currentDate 도 참조하지만 시작일 변경 시 종료측 검증을 다시 돌리지
|
|
143
|
+
// 않는 건 의도된 것이다: 시작일 변경은 위 effect 가 type:'start' 로 처리하며, 여기에 start 를
|
|
144
|
+
// deps 로 넣으면 1c5ac7ad 가 제거한 그 루프가 재발한다.
|
|
145
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
124
146
|
}, [endDateOptions.currentDate]);
|
|
125
147
|
return _jsxs("div", {
|
|
126
148
|
className: "ncua-range-date-picker",
|