@ncds/ui-admin 1.8.13 → 1.8.15

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.
Files changed (91) hide show
  1. package/dist/cjs/src/components/forms-and-input/checkbox/CheckboxInput.js +3 -2
  2. package/dist/cjs/src/components/forms-and-input/date-picker/DatePicker.js +17 -20
  3. package/dist/cjs/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.js +53 -0
  4. package/dist/cjs/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.js +129 -0
  5. package/dist/cjs/src/components/forms-and-input/range-date-picker/RangeDatePicker.js +22 -0
  6. package/dist/cjs/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.js +95 -0
  7. package/dist/cjs/src/components/forms-and-input/range-date-picker-with-buttons/RangeDatePickerWithButtons.js +7 -1
  8. package/dist/cjs/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.js +57 -0
  9. package/dist/cjs/src/components/forms-and-input/select/Select.js +12 -1
  10. package/dist/cjs/src/components/forms-and-input/select-box/SelectBox.js +5 -2
  11. package/dist/cjs/src/components/forms-and-input/slider/Slider.js +7 -1
  12. package/dist/cjs/src/components/navigation/pagination/Pagination.js +12 -2
  13. package/dist/cjs/src/components/navigation/pagination/__tests__/Pagination.test.js +62 -0
  14. package/dist/cjs/src/components/overlays/tooltip/Tooltip.js +15 -2
  15. package/dist/cjs/src/components/overlays/tooltip/__tests__/utils.test.js +40 -0
  16. package/dist/cjs/src/components/overlays/tooltip/utils.js +20 -0
  17. package/dist/cjs/src/hooks/index.js +11 -0
  18. package/dist/cjs/src/hooks/useIsomorphicLayoutEffect.js +8 -0
  19. package/dist/cjs/src/hooks/useMediaQuery.js +3 -0
  20. package/dist/cjs/src/utils/__tests__/date-picker.test.js +29 -0
  21. package/dist/cjs/src/utils/date-picker.js +15 -1
  22. package/dist/esm/src/components/forms-and-input/checkbox/CheckboxInput.js +4 -3
  23. package/dist/esm/src/components/forms-and-input/date-picker/DatePicker.js +18 -21
  24. package/dist/esm/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.js +50 -0
  25. package/dist/esm/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.js +126 -0
  26. package/dist/esm/src/components/forms-and-input/range-date-picker/RangeDatePicker.js +22 -0
  27. package/dist/esm/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.js +92 -0
  28. package/dist/esm/src/components/forms-and-input/range-date-picker-with-buttons/RangeDatePickerWithButtons.js +7 -1
  29. package/dist/esm/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.js +54 -0
  30. package/dist/esm/src/components/forms-and-input/select/Select.js +12 -1
  31. package/dist/esm/src/components/forms-and-input/select-box/SelectBox.js +5 -2
  32. package/dist/esm/src/components/forms-and-input/slider/Slider.js +7 -1
  33. package/dist/esm/src/components/navigation/pagination/Pagination.js +12 -2
  34. package/dist/esm/src/components/navigation/pagination/__tests__/Pagination.test.js +59 -0
  35. package/dist/esm/src/components/overlays/tooltip/Tooltip.js +15 -2
  36. package/dist/esm/src/components/overlays/tooltip/__tests__/utils.test.js +38 -0
  37. package/dist/esm/src/components/overlays/tooltip/utils.js +13 -0
  38. package/dist/esm/src/hooks/index.js +1 -0
  39. package/dist/esm/src/hooks/useIsomorphicLayoutEffect.js +2 -0
  40. package/dist/esm/src/hooks/useMediaQuery.js +3 -0
  41. package/dist/esm/src/utils/__tests__/date-picker.test.js +26 -0
  42. package/dist/esm/src/utils/date-picker.js +13 -0
  43. package/dist/temp/src/components/forms-and-input/checkbox/CheckboxInput.js +4 -3
  44. package/dist/temp/src/components/forms-and-input/date-picker/DatePicker.js +18 -28
  45. package/dist/temp/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.d.ts +1 -0
  46. package/dist/temp/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.js +43 -0
  47. package/dist/temp/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.d.ts +1 -0
  48. package/dist/temp/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.js +109 -0
  49. package/dist/temp/src/components/forms-and-input/range-date-picker/RangeDatePicker.js +22 -0
  50. package/dist/temp/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.d.ts +1 -0
  51. package/dist/temp/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.js +60 -0
  52. package/dist/temp/src/components/forms-and-input/range-date-picker-with-buttons/RangeDatePickerWithButtons.js +9 -1
  53. package/dist/temp/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.d.ts +1 -0
  54. package/dist/temp/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.js +42 -0
  55. package/dist/temp/src/components/forms-and-input/select/Select.d.ts +1 -0
  56. package/dist/temp/src/components/forms-and-input/select/Select.js +10 -2
  57. package/dist/temp/src/components/forms-and-input/select-box/SelectBox.d.ts +1 -0
  58. package/dist/temp/src/components/forms-and-input/select-box/SelectBox.js +5 -4
  59. package/dist/temp/src/components/forms-and-input/slider/Slider.js +7 -1
  60. package/dist/temp/src/components/navigation/pagination/Pagination.js +12 -2
  61. package/dist/temp/src/components/navigation/pagination/__tests__/Pagination.test.d.ts +1 -0
  62. package/dist/temp/src/components/navigation/pagination/__tests__/Pagination.test.js +59 -0
  63. package/dist/temp/src/components/overlays/tooltip/Tooltip.js +21 -4
  64. package/dist/temp/src/components/overlays/tooltip/__tests__/utils.test.d.ts +1 -0
  65. package/dist/temp/src/components/overlays/tooltip/__tests__/utils.test.js +38 -0
  66. package/dist/temp/src/components/overlays/tooltip/utils.d.ts +9 -0
  67. package/dist/temp/src/components/overlays/tooltip/utils.js +13 -0
  68. package/dist/temp/src/hooks/index.d.ts +1 -0
  69. package/dist/temp/src/hooks/index.js +1 -0
  70. package/dist/temp/src/hooks/useIsomorphicLayoutEffect.d.ts +2 -0
  71. package/dist/temp/src/hooks/useIsomorphicLayoutEffect.js +2 -0
  72. package/dist/temp/src/hooks/useMediaQuery.js +3 -0
  73. package/dist/temp/src/utils/__tests__/date-picker.test.d.ts +1 -0
  74. package/dist/temp/src/utils/__tests__/date-picker.test.js +26 -0
  75. package/dist/temp/src/utils/date-picker.d.ts +2 -0
  76. package/dist/temp/src/utils/date-picker.js +13 -0
  77. package/dist/types/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.d.ts +1 -0
  78. package/dist/types/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.d.ts +1 -0
  79. package/dist/types/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.d.ts +1 -0
  80. package/dist/types/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.d.ts +1 -0
  81. package/dist/types/src/components/forms-and-input/select/Select.d.ts +1 -0
  82. package/dist/types/src/components/forms-and-input/select-box/SelectBox.d.ts +1 -0
  83. package/dist/types/src/components/navigation/pagination/__tests__/Pagination.test.d.ts +1 -0
  84. package/dist/types/src/components/overlays/tooltip/__tests__/utils.test.d.ts +1 -0
  85. package/dist/types/src/components/overlays/tooltip/utils.d.ts +9 -0
  86. package/dist/types/src/hooks/index.d.ts +1 -0
  87. package/dist/types/src/hooks/useIsomorphicLayoutEffect.d.ts +2 -0
  88. package/dist/types/src/utils/__tests__/date-picker.test.d.ts +1 -0
  89. package/dist/types/src/utils/date-picker.d.ts +2 -0
  90. package/dist/ui-admin/assets/styles/style.css +33 -21
  91. package/package.json +2 -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, _react.useEffect)(() => {
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", {
@@ -183,6 +183,12 @@ const setupTimeInputHandlers = (instance, input, isPortal, onHourInput, onMinute
183
183
  };
184
184
  /** 시간 입력 필드 이벤트 리스너 정리 */
185
185
  const cleanupTimeInputHandlers = (instance, input, isPortal, onHourInput, onMinuteInput) => {
186
+ // document 전역 리스너는 캘린더 DOM 조회 성공 여부와 무관하게 항상 먼저 제거한다 (누수 방지)
187
+ const handleMouseDown = instance._handleMouseDown;
188
+ if (handleMouseDown) {
189
+ document.removeEventListener('mousedown', handleMouseDown, true);
190
+ instance._handleMouseDown = undefined;
191
+ }
186
192
  const timeContainer = isPortal ? instance.calendarContainer : input.parentElement;
187
193
  const timeInputWrapper = timeContainer?.querySelector('.flatpickr-time');
188
194
  if (!timeInputWrapper) return;
@@ -194,10 +200,6 @@ const cleanupTimeInputHandlers = (instance, input, isPortal, onHourInput, onMinu
194
200
  if (minuteInput) {
195
201
  minuteInput.removeEventListener('input', onMinuteInput);
196
202
  }
197
- const handleMouseDown = instance._handleMouseDown;
198
- if (handleMouseDown) {
199
- document.removeEventListener('mousedown', handleMouseDown, true);
200
- }
201
203
  };
202
204
  // ══════════════════════════════════════════
203
205
  // DatePicker 컴포넌트
@@ -227,7 +229,7 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
227
229
  const onValidationErrorRef = (0, _react.useRef)(onValidationError);
228
230
  onValidationErrorRef.current = onValidationError;
229
231
  /** portal 컨테이너를 lazily 생성 (이미 있으면 재사용), className은 매번 갱신 */
230
- const getPortalContainer = () => {
232
+ const getPortalContainer = (0, _react.useCallback)(() => {
231
233
  if (!portal || typeof document === 'undefined') return undefined;
232
234
  const portalClassName = (0, _classnames.default)('ncua-date-picker', 'ncua-date-picker--portal', `ncua-date-picker--${size}`, {
233
235
  'ncua-date-picker--has-time': hasTimeOption
@@ -243,7 +245,7 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
243
245
  document.body.appendChild(el);
244
246
  portalContainerRef.current = el;
245
247
  return el;
246
- };
248
+ }, [portal, size, hasTimeOption]);
247
249
  /** 컴포넌트 언마운트 시 포탈 컨테이너 및 스크롤 리스너 정리 */
248
250
  (0, _react.useEffect)(() => {
249
251
  return () => {
@@ -255,7 +257,7 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
255
257
  // 날짜 유효성 검사
256
258
  // ──────────────────────────────────────────────
257
259
  /** minDate/maxDate 범위를 벗어났는지 확인 */
258
- const checkDateViolations = (date, minDate, maxDate) => {
260
+ const checkDateViolations = (0, _react.useCallback)((date, minDate, maxDate) => {
259
261
  const violations = [];
260
262
  const inputDate = (0, _moment.default)(date);
261
263
  if (!inputDate.isValid()) return violations;
@@ -272,16 +274,16 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
272
274
  }
273
275
  }
274
276
  return violations;
275
- };
277
+ }, []);
276
278
  // ──────────────────────────────────────────────
277
279
  // 이벤트 핸들러
278
280
  // ──────────────────────────────────────────────
279
281
  /** 유효하지 않은 입력 시 이전 날짜로 복원 */
280
- const restorePreviousDate = (target, instance) => {
282
+ const restorePreviousDate = (0, _react.useCallback)((target, instance) => {
281
283
  if (instance.selectedDates.length > 0) {
282
284
  const prevDate = instance.selectedDates[0];
283
285
  if (prevDate instanceof Date && !Number.isNaN(prevDate.getTime())) {
284
- const momentFormat = dateFormatRef.current.replace(/Y/g, 'YYYY').replace(/m/g, 'MM').replace(/d/g, 'DD');
286
+ const momentFormat = (0, _datePicker.convertToMomentFormat)(dateFormatRef.current);
285
287
  target.value = (0, _moment.default)(prevDate).format(momentFormat);
286
288
  instance.setDate(prevDate, false);
287
289
  return;
@@ -289,7 +291,7 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
289
291
  }
290
292
  target.value = '';
291
293
  instance.setDate('', false);
292
- };
294
+ }, []);
293
295
  /** flatpickr에서 날짜가 변경되었을 때 호출 */
294
296
  const onChangeDateHandler = (0, _react.useCallback)((dateTimeStamp, dateStr, fpInstance) => {
295
297
  const instance = fpInstance;
@@ -303,7 +305,9 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
303
305
  const maxDate = instance.config.maxDate;
304
306
  const violations = checkDateViolations(selectedDate, minDate, maxDate);
305
307
  if (violations.length > 0 && onValidationErrorRef.current) {
306
- const prevDate = instance?.selectedDates?.[0];
308
+ // flatpickr는 onChange 발화 전에 selectedDates를 위반된 새 날짜로 갱신하므로,
309
+ // 직전 유효 날짜를 보관해 둔 _previousDateBeforeInput을 previousDate로 사용한다
310
+ const prevDate = instance._previousDateBeforeInput;
307
311
  const validPrevDate = prevDate instanceof Date && !Number.isNaN(prevDate.getTime()) ? prevDate : undefined;
308
312
  onValidationErrorRef.current({
309
313
  date: selectedDate,
@@ -319,7 +323,6 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
319
323
  isValidDate(formattedDate) ? onChangeDateRef.current(formattedDate) : onChangeDateRef.current(dateStr);
320
324
  }, [checkDateViolations, restorePreviousDate]);
321
325
  /** input에 직접 타이핑할 때 날짜 형식 자동 변환 및 유효성 검사 */
322
- // eslint-disable-next-line react-hooks/exhaustive-deps
323
326
  const onInputHandler = (0, _react.useCallback)(e => {
324
327
  const target = e.target;
325
328
  const input = target.value;
@@ -372,12 +375,6 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
372
375
  // ──────────────────────────────────────────────
373
376
  // flatpickr 옵션
374
377
  // ──────────────────────────────────────────────
375
- // datePickerOptions는 부모에서 매 렌더 새 객체일 수 있으므로 값 기반 비교
376
- // 주의: JSON.stringify는 함수를 무시하고 Date를 문자열로 변환하므로,
377
- // datePickerOptions에 함수나 Date 객체를 넘기면 변경 감지가 누락될 수 있음
378
- // eslint-disable-next-line react-hooks/exhaustive-deps
379
- const _datePickerOptionsKey = JSON.stringify(datePickerOptions);
380
- // eslint-disable-next-line react-hooks/exhaustive-deps
381
378
  const options = (0, _react.useMemo)(() => ({
382
379
  mode: 'single',
383
380
  static: !portal,
@@ -398,7 +395,7 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
398
395
  if (!momentDate.isValid()) {
399
396
  return '';
400
397
  }
401
- const momentFormat = format.replace(/Y/g, 'YYYY').replace(/y/g, 'YY').replace(/m/g, 'MM').replace(/d/g, 'DD').replace(/H/g, 'HH').replace(/i/g, 'mm').replace(/S/g, 'ss');
398
+ const momentFormat = (0, _datePicker.convertToMomentFormat)(format);
402
399
  return momentDate.format(momentFormat);
403
400
  } catch (_error) {
404
401
  return '';
@@ -0,0 +1,53 @@
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 _DatePicker = require("../DatePicker");
8
+ // @vitest-environment jsdom
9
+
10
+ const {
11
+ captured
12
+ } = _vitest.vi.hoisted(() => ({
13
+ captured: []
14
+ }));
15
+ // react-flatpickr를 mock하여 DatePicker가 넘기는 options 참조를 렌더마다 수집한다.
16
+ // (vi.mock / vi.hoisted는 vitest가 import 위로 호이스팅하므로 DatePicker import보다 먼저 적용된다)
17
+ _vitest.vi.mock('react-flatpickr', () => ({
18
+ default: props => {
19
+ captured.push(props.options);
20
+ return null;
21
+ }
22
+ }));
23
+ (0, _vitest.describe)('#1 options useMemo 안정성', () => {
24
+ (0, _vitest.it)('부모가 같은 props로 강제 리렌더해도 options 참조가 동일하게 유지된다(재생성 안 됨)', () => {
25
+ let bump = () => undefined;
26
+ const Harness = () => {
27
+ const [, setN] = (0, _react.useState)(0);
28
+ bump = () => setN(x => x + 1);
29
+ return /*#__PURE__*/(0, _react.createElement)(_DatePicker.DatePicker, {
30
+ currentDate: '2024-03-20',
31
+ onChangeDate: () => undefined
32
+ });
33
+ };
34
+ const container = document.createElement('div');
35
+ document.body.appendChild(container);
36
+ const root = (0, _client.createRoot)(container);
37
+ (0, _testUtils.act)(() => {
38
+ root.render(/*#__PURE__*/(0, _react.createElement)(Harness));
39
+ });
40
+ const countAfterMount = captured.length;
41
+ const firstOptions = captured[captured.length - 1];
42
+ (0, _testUtils.act)(() => {
43
+ bump();
44
+ });
45
+ const lastOptions = captured[captured.length - 1];
46
+ (0, _vitest.expect)(captured.length).toBeGreaterThan(countAfterMount); // 리렌더가 실제로 일어났는지
47
+ (0, _vitest.expect)(Object.is(firstOptions, lastOptions)).toBe(true); // 동일 참조 = options 재생성 안 됨
48
+ (0, _testUtils.act)(() => {
49
+ root.unmount();
50
+ });
51
+ container.remove();
52
+ });
53
+ });
@@ -0,0 +1,129 @@
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 _DatePicker = require("../DatePicker");
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)(_DatePicker.DatePicker, 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)('DatePicker 렌더 (스모크)', () => {
37
+ (0, _vitest.it)('기본 props로 input을 렌더하고 크래시하지 않는다', () => {
38
+ const container = mount({
39
+ currentDate: '2024-03-20',
40
+ onChangeDate: () => undefined
41
+ });
42
+ (0, _vitest.expect)(container.querySelector('input')).not.toBeNull();
43
+ });
44
+ (0, _vitest.it)('enableTime 옵션으로도 정상 렌더한다', () => {
45
+ const container = mount({
46
+ currentDate: '2024-03-20 14:30',
47
+ onChangeDate: () => undefined,
48
+ datePickerOptions: {
49
+ enableTime: true,
50
+ dateFormat: 'Y-m-d H:i'
51
+ }
52
+ });
53
+ (0, _vitest.expect)(container.querySelector('input')).not.toBeNull();
54
+ });
55
+ });
56
+ (0, _vitest.describe)('#4 document mousedown 리스너 정리', () => {
57
+ (0, _vitest.it)('enableTime 마운트 후 언마운트 시, 추가된 mousedown 리스너가 모두 제거된다(누수 없음)', () => {
58
+ const added = [];
59
+ const removed = [];
60
+ const origAdd = document.addEventListener.bind(document);
61
+ const origRemove = document.removeEventListener.bind(document);
62
+ _vitest.vi.spyOn(document, 'addEventListener').mockImplementation((type, fn, opts) => {
63
+ if (type === 'mousedown' && fn) added.push(fn);
64
+ return origAdd(type, fn, opts);
65
+ });
66
+ _vitest.vi.spyOn(document, 'removeEventListener').mockImplementation((type, fn, opts) => {
67
+ if (type === 'mousedown' && fn) removed.push(fn);
68
+ return origRemove(type, fn, opts);
69
+ });
70
+ const container = document.createElement('div');
71
+ document.body.appendChild(container);
72
+ const root = (0, _client.createRoot)(container);
73
+ (0, _testUtils.act)(() => {
74
+ root.render(/*#__PURE__*/(0, _react.createElement)(_DatePicker.DatePicker, {
75
+ currentDate: '2024-03-20 14:30',
76
+ onChangeDate: () => undefined,
77
+ datePickerOptions: {
78
+ enableTime: true,
79
+ dateFormat: 'Y-m-d H:i'
80
+ }
81
+ }));
82
+ });
83
+ (0, _vitest.expect)(added.length).toBeGreaterThanOrEqual(1);
84
+ (0, _testUtils.act)(() => {
85
+ root.unmount();
86
+ });
87
+ container.remove();
88
+ for (const fn of added) {
89
+ (0, _vitest.expect)(removed).toContain(fn);
90
+ }
91
+ });
92
+ });
93
+ (0, _vitest.describe)('#3 onValidationError.previousDate', () => {
94
+ // 허용 범위(10~25일) 기준: 마지막 유효 선택일과 범위 밖 위반일
95
+ const LAST_VALID_DAY = 15;
96
+ const OUT_OF_RANGE_DAY = 30;
97
+ (0, _vitest.it)('min/max 위반 시 previousDate는 위반 날짜가 아니라 직전 유효 날짜다', () => {
98
+ const onValidationError = _vitest.vi.fn();
99
+ const container = mount({
100
+ currentDate: '2024-03-20',
101
+ onChangeDate: () => undefined,
102
+ datePickerOptions: {
103
+ minDate: '2024-03-10',
104
+ maxDate: '2024-03-25',
105
+ allowInput: true
106
+ },
107
+ onValidationError
108
+ });
109
+ // flatpickr 인스턴스는 초기화된 input 엘리먼트의 _flatpickr에 부착된다
110
+ const input = container.querySelector('input');
111
+ const instance = input?._flatpickr;
112
+ (0, _vitest.expect)(instance).toBeTruthy();
113
+ // 1) 유효 날짜를 먼저 선택 → 직전 유효값(_previousDateBeforeInput)이 확립됨
114
+ (0, _testUtils.act)(() => {
115
+ instance.setDate(`2024-03-${LAST_VALID_DAY}`, true);
116
+ });
117
+ (0, _vitest.expect)(onValidationError).not.toHaveBeenCalled();
118
+ // 2) 허용 범위 밖 날짜 선택 → 위반 분기 진입
119
+ (0, _testUtils.act)(() => {
120
+ instance.setDate(`2024-03-${OUT_OF_RANGE_DAY}`, true);
121
+ });
122
+ (0, _vitest.expect)(onValidationError).toHaveBeenCalledTimes(1);
123
+ const arg = onValidationError.mock.calls[0][0];
124
+ (0, _vitest.expect)(arg.violations).toContain('maxDate');
125
+ (0, _vitest.expect)(arg.date.getDate()).toBe(OUT_OF_RANGE_DAY); // 위반 날짜
126
+ (0, _vitest.expect)(arg.previousDate?.getDate()).toBe(LAST_VALID_DAY); // 직전 유효 날짜 (≠ 위반 날짜)
127
+ (0, _vitest.expect)(arg.previousDate?.getTime()).not.toBe(arg.date.getTime());
128
+ });
129
+ });
@@ -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",
@@ -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
+ });
@@ -24,12 +24,14 @@ const Select = exports.Select = /*#__PURE__*/(0, _react.forwardRef)((_ref, ref)
24
24
  optionItems,
25
25
  register,
26
26
  disabled = false,
27
+ readOnly = false,
27
28
  ...props
28
29
  } = _ref;
29
30
  return (0, _jsxRuntime.jsxs)("span", {
30
31
  className: (0, _classnames.default)('ncua-select', {
31
32
  destructive: destructive,
32
- 'ncua-select--simple': type === 'simple'
33
+ 'ncua-select--simple': type === 'simple',
34
+ 'ncua-select--readonly': readOnly
33
35
  }, className, `ncua-select--${size}`),
34
36
  children: [(0, _jsxRuntime.jsx)("span", {
35
37
  className: "ncua-select__content",
@@ -39,8 +41,17 @@ const Select = exports.Select = /*#__PURE__*/(0, _react.forwardRef)((_ref, ref)
39
41
  id: id,
40
42
  className: (0, _classnames.default)('ncua-select__tag', className),
41
43
  disabled: disabled,
44
+ "aria-readonly": readOnly,
42
45
  ...register,
43
46
  ...props,
47
+ onKeyDown: e => {
48
+ props.onKeyDown?.(e);
49
+ // native select에는 readonly 속성이 없어 키보드로 값이 바뀐다.
50
+ // 값 변경 키는 막고, Tab/Shift+Tab(포커스 이동)은 허용한다.
51
+ if (readOnly && e.key !== 'Tab') {
52
+ e.preventDefault();
53
+ }
54
+ },
44
55
  children: [placeholder && (0, _jsxRuntime.jsx)("option", {
45
56
  value: "",
46
57
  disabled: disabledPlaceholder,
@@ -78,6 +78,7 @@ const SelectBox = exports.SelectBox = /*#__PURE__*/(0, _react.forwardRef)((_ref2
78
78
  value,
79
79
  optionItems = [],
80
80
  disabled = false,
81
+ readOnly = false,
81
82
  maxHeight = DEFAULT_MAX_HEIGHT,
82
83
  multiple = false,
83
84
  maxSelection,
@@ -110,7 +111,7 @@ const SelectBox = exports.SelectBox = /*#__PURE__*/(0, _react.forwardRef)((_ref2
110
111
  return selectedOption ? selectedOption : placeholder;
111
112
  }, [multiple, selectedOption, placeholder]);
112
113
  const handleOptionSelect = option => {
113
- if (disabled) return;
114
+ if (disabled || readOnly) return;
114
115
  if (multiple) {
115
116
  const newValue = tryToggle(option.id, Array.isArray(value) ? value : []);
116
117
  if (newValue === null) return;
@@ -139,7 +140,7 @@ const SelectBox = exports.SelectBox = /*#__PURE__*/(0, _react.forwardRef)((_ref2
139
140
  maxHeight,
140
141
  itemCount: optionItems.length,
141
142
  optionItems,
142
- disabled,
143
+ disabled: disabled || readOnly,
143
144
  multiple,
144
145
  onSelect: handleOptionSelect
145
146
  });
@@ -244,6 +245,7 @@ const SelectBox = exports.SelectBox = /*#__PURE__*/(0, _react.forwardRef)((_ref2
244
245
  className: (0, _classnames.default)('ncua-selectbox', `ncua-selectbox--${size}`, {
245
246
  'ncua-selectbox--open': isOpen,
246
247
  'ncua-selectbox--disabled': disabled,
248
+ 'ncua-selectbox--readonly': readOnly,
247
249
  'ncua-selectbox--simple': type === 'simple',
248
250
  'ncua-selectbox--multiple': multiple,
249
251
  destructive: destructive
@@ -259,6 +261,7 @@ const SelectBox = exports.SelectBox = /*#__PURE__*/(0, _react.forwardRef)((_ref2
259
261
  "aria-haspopup": "listbox",
260
262
  "aria-controls": `selectbox-options-${id || 'default'}`,
261
263
  "aria-disabled": disabled,
264
+ "aria-readonly": readOnly,
262
265
  "aria-label": selectedOption ? selectedOption.label : placeholder,
263
266
  "aria-activedescendant": activeDescendantId,
264
267
  children: [(0, _jsxRuntime.jsxs)("div", {
@@ -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
- }, [typeof value, onChange, min, max, step, labelPosition]);
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);