@ncds/ui-admin 1.8.17 → 1.8.18

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 (21) hide show
  1. package/dist/cjs/src/components/forms-and-input/date-picker/DatePicker.js +174 -38
  2. package/dist/cjs/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.js +410 -0
  3. package/dist/cjs/src/components/forms-and-input/range-date-picker/RangeDatePicker.js +9 -6
  4. package/dist/cjs/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.js +61 -0
  5. package/dist/cjs/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.js +249 -0
  6. package/dist/cjs/vitest.config.js +23 -1
  7. package/dist/esm/src/components/forms-and-input/date-picker/DatePicker.js +174 -38
  8. package/dist/esm/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.js +410 -0
  9. package/dist/esm/src/components/forms-and-input/range-date-picker/RangeDatePicker.js +9 -6
  10. package/dist/esm/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.js +61 -0
  11. package/dist/esm/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.js +250 -1
  12. package/dist/esm/vitest.config.js +22 -1
  13. package/dist/temp/src/components/forms-and-input/date-picker/DatePicker.js +184 -36
  14. package/dist/temp/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.js +323 -0
  15. package/dist/temp/src/components/forms-and-input/range-date-picker/RangeDatePicker.d.ts +9 -0
  16. package/dist/temp/src/components/forms-and-input/range-date-picker/RangeDatePicker.js +9 -6
  17. package/dist/temp/src/components/forms-and-input/range-date-picker/__tests__/RangeDatePicker.test.js +40 -0
  18. package/dist/temp/src/components/forms-and-input/range-date-picker-with-buttons/__tests__/RangeDatePickerWithButtons.test.js +190 -1
  19. package/dist/temp/vitest.config.js +24 -0
  20. package/dist/types/src/components/forms-and-input/range-date-picker/RangeDatePicker.d.ts +9 -0
  21. package/package.json +5 -3
@@ -58,6 +58,23 @@ const restoreIfInvalidDate = (date, instance) => {
58
58
  }
59
59
  return true;
60
60
  };
61
+ /** Date 를 dateFormat 표기 문자열로 바꾼다. 유효하지 않으면 null. */
62
+ const formatSelectedDate = (selectedDate, dateFormat) => {
63
+ if (!(selectedDate instanceof Date) || Number.isNaN(selectedDate.getTime())) return null;
64
+ const formattedDate = (0, _moment.default)(selectedDate).format((0, _datePicker.convertToMomentFormat)(dateFormat));
65
+ return formattedDate || null;
66
+ };
67
+ /** 'YYYY-MM-DD' 길이. dateFormat 을 읽기 전(onReady 이전)의 기본값이다 */
68
+ const FULL_DATE_LENGTH = 10;
69
+ /** 연·월·일·시·분·초가 모두 두 자리 이상인 기준 시각. 길이 측정용이라 날짜 값 자체에 의미는 없다 */
70
+ const FULL_VALUE_REFERENCE_DATE = '2026-03-01 04:05:06';
71
+ /**
72
+ * 해당 dateFormat 으로 완성된 값의 길이. 'Y-m-d'=10, 'Y-m-d H:i'=16, 'H:i'=5.
73
+ *
74
+ * 완성 판정을 10 으로 고정하면 enableTime(16자) 에서 아직 덜 친 중간 상태를 완성으로 오판한다.
75
+ * 그러면 `2026-03-01 0` 같은 값이 유효성 검사에 걸려 입력이 통째로 이전 값으로 되돌아간다.
76
+ */
77
+ const getFullValueLength = dateFormat => (0, _moment.default)(FULL_VALUE_REFERENCE_DATE).format((0, _datePicker.convertToMomentFormat)(dateFormat)).length || FULL_DATE_LENGTH;
61
78
  /** 시간 전용 모드의 기본값 반환 */
62
79
  const getTimeOnlyDefault = (hasSeconds, isEndDate) => {
63
80
  const endTime = hasSeconds ? '23:59:59' : '23:59';
@@ -219,7 +236,10 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
219
236
  } = _ref;
220
237
  const flatpickrInstanceRef = (0, _react.useRef)(null);
221
238
  const dateFormatRef = (0, _react.useRef)('Y-m-d');
222
- const minMaxDateRef = (0, _react.useRef)({});
239
+ /** 현재 dateFormat 기준 완성 값의 길이 (getFullValueLength 주석 참고) */
240
+ const fullValueLengthRef = (0, _react.useRef)(FULL_DATE_LENGTH);
241
+ /** 인스턴스 파괴 직전에 input 이 포커스를 쥐고 있었는지 */
242
+ const hadFocusBeforeDestroyRef = (0, _react.useRef)(false);
223
243
  /** portal 모드: 캘린더를 담을 persistent 컨테이너 (한 번 생성, 언마운트 시 제거) */
224
244
  const portalContainerRef = (0, _react.useRef)(null);
225
245
  const hasTimeOption = datePickerOptions && Object.hasOwn(datePickerOptions, 'enableTime');
@@ -228,6 +248,12 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
228
248
  onChangeDateRef.current = onChangeDate;
229
249
  const onValidationErrorRef = (0, _react.useRef)(onValidationError);
230
250
  onValidationErrorRef.current = onValidationError;
251
+ /** flatpickr에 실제로 로드된 날짜 문자열. onReady 클로저는 최초 렌더 값에 고정되므로 ref로 최신값을 본다 */
252
+ const loadedDateRef = (0, _react.useRef)('');
253
+ /** 마지막으로 부모에 통지한 날짜. blur 동기화의 중복 통지를 막는다 */
254
+ const lastNotifiedDateRef = (0, _react.useRef)(null);
255
+ /** blur 동기화 지연 타이머 */
256
+ const blurSyncTimerRef = (0, _react.useRef)(null);
231
257
  /** portal 컨테이너를 lazily 생성 (이미 있으면 재사용), className은 매번 갱신 */
232
258
  const getPortalContainer = (0, _react.useCallback)(() => {
233
259
  if (!portal || typeof document === 'undefined') return undefined;
@@ -246,11 +272,15 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
246
272
  portalContainerRef.current = el;
247
273
  return el;
248
274
  }, [portal, size, hasTimeOption]);
249
- /** 컴포넌트 언마운트 시 포탈 컨테이너 및 스크롤 리스너 정리 */
275
+ /** 컴포넌트 언마운트 시 포탈 컨테이너 및 지연 타이머 정리 */
250
276
  (0, _react.useEffect)(() => {
251
277
  return () => {
252
278
  portalContainerRef.current?.remove();
253
279
  portalContainerRef.current = null;
280
+ if (blurSyncTimerRef.current) {
281
+ clearTimeout(blurSyncTimerRef.current);
282
+ blurSyncTimerRef.current = null;
283
+ }
254
284
  };
255
285
  }, []);
256
286
  // ──────────────────────────────────────────────
@@ -292,6 +322,66 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
292
322
  target.value = '';
293
323
  instance.setDate('', false);
294
324
  }, []);
325
+ /**
326
+ * 부모가 이미 그 값을 알고 있으면 true — 통지할 것이 없다.
327
+ *
328
+ * flatpickr 는 값을 확정하는 경로가 4개(캘린더 선택·Enter·타이핑·바깥클릭)이고 서로의 실행을
329
+ * 모른다. 그래서 같은 값이 여러 경로에서 중복 통지되기 쉬운데, 판정을 이 한 곳에만 둔다.
330
+ * - loadedDate: 부모가 prop 으로 들고 있는 값과 같으면 바뀐 게 없다
331
+ * - lastNotifiedDate: 부모가 state 를 갱신하지 않는 사용처(uncontrolled)에서도 중복을 막는다
332
+ */
333
+ const isAlreadyNotified = (0, _react.useCallback)(nextDate => nextDate === loadedDateRef.current || nextDate === lastNotifiedDateRef.current, []);
334
+ /**
335
+ * 부모로 날짜를 통지하는 유일한 창구. 통지했으면 true.
336
+ * lastNotifiedDateRef 를 쓰는 곳도 여기 하나뿐이다.
337
+ */
338
+ const commitDate = (0, _react.useCallback)(nextDate => {
339
+ if (isAlreadyNotified(nextDate)) return false;
340
+ lastNotifiedDateRef.current = nextDate;
341
+ onChangeDateRef.current(nextDate);
342
+ return true;
343
+ }, [isAlreadyNotified]);
344
+ /**
345
+ * min/max 위반이면 onValidationError 로 보고하고 true(통지 중단)를 반환한다.
346
+ * 캘린더 선택 경로(onChangeDateHandler)와 동일한 방어를 blur 동기화에도 적용하기 위한 것으로,
347
+ * 이 검사가 없으면 범위 밖 날짜를 타이핑한 뒤 마우스로 빠져나갈 때 방어가 뚫린다.
348
+ */
349
+ const reportViolationIfAny = (0, _react.useCallback)((selectedDate, instance, previousDate) => {
350
+ const minDate = instance.config.minDate;
351
+ const maxDate = instance.config.maxDate;
352
+ const violations = checkDateViolations(selectedDate, minDate, maxDate);
353
+ if (violations.length === 0 || !onValidationErrorRef.current) return false;
354
+ const validPreviousDate = previousDate instanceof Date && !Number.isNaN(previousDate.getTime()) ? previousDate : undefined;
355
+ onValidationErrorRef.current({
356
+ date: selectedDate,
357
+ minDate,
358
+ maxDate,
359
+ violations,
360
+ previousDate: validPreviousDate
361
+ });
362
+ return true;
363
+ }, [checkDateViolations]);
364
+ /**
365
+ * blur 이후 flatpickr가 끝내 통지하지 않은 값을 부모로 올린다.
366
+ *
367
+ * flatpickr는 경로마다 onChange 발화 시점이 다르다. Enter/Tab 은 blur 보다 앞이고, 캘린더 선택은
368
+ * blur 보다 뒤다. 그래서 blur 시점에 동기적으로 판정하면 어느 한쪽이 반드시 중복 통지된다.
369
+ * 한 틱 미뤄 "결국 아무도 통지하지 않은 경우"만 남겨서 처리한다.
370
+ */
371
+ const scheduleBlurSync = (0, _react.useCallback)(previousDate => {
372
+ if (blurSyncTimerRef.current) clearTimeout(blurSyncTimerRef.current);
373
+ blurSyncTimerRef.current = setTimeout(() => {
374
+ blurSyncTimerRef.current = null;
375
+ const instance = flatpickrInstanceRef.current;
376
+ if (!instance || instance.selectedDates.length === 0) return;
377
+ const selectedDate = instance.selectedDates[0];
378
+ const dateToNotify = formatSelectedDate(selectedDate, dateFormatRef.current);
379
+ // 바뀐 게 없으면 위반 보고도 하지 않는다. 안 그러면 blur 마다 알럿이 반복된다
380
+ if (dateToNotify === null || isAlreadyNotified(dateToNotify)) return;
381
+ if (reportViolationIfAny(selectedDate, instance, previousDate)) return;
382
+ commitDate(dateToNotify);
383
+ }, 0);
384
+ }, [commitDate, isAlreadyNotified, reportViolationIfAny]);
295
385
  /** flatpickr에서 날짜가 변경되었을 때 호출 */
296
386
  const onChangeDateHandler = (0, _react.useCallback)((dateTimeStamp, dateStr, fpInstance) => {
297
387
  const instance = fpInstance;
@@ -301,27 +391,38 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
301
391
  }
302
392
  if (restoreIfInvalidDate(dateTimeStamp[0], instance)) return;
303
393
  const selectedDate = dateTimeStamp[0];
304
- const minDate = instance.config.minDate;
305
- const maxDate = instance.config.maxDate;
306
- const violations = checkDateViolations(selectedDate, minDate, maxDate);
307
- if (violations.length > 0 && onValidationErrorRef.current) {
308
- // flatpickr는 onChange 발화 전에 selectedDates를 위반된 새 날짜로 갱신하므로,
309
- // 직전 유효 날짜를 보관해 둔 _previousDateBeforeInput을 previousDate로 사용한다
310
- const prevDate = instance._previousDateBeforeInput;
311
- const validPrevDate = prevDate instanceof Date && !Number.isNaN(prevDate.getTime()) ? prevDate : undefined;
312
- onValidationErrorRef.current({
313
- date: selectedDate,
314
- minDate,
315
- maxDate,
316
- violations,
317
- previousDate: validPrevDate
318
- });
319
- return;
320
- }
394
+ // flatpickr는 onChange 발화 전에 selectedDates를 위반된 새 날짜로 갱신하므로,
395
+ // 직전 유효 날짜를 보관해 둔 _previousDateBeforeInput을 previousDate로 넘긴다
396
+ if (reportViolationIfAny(selectedDate, instance, instance._previousDateBeforeInput)) return;
321
397
  instance._previousDateBeforeInput = selectedDate;
322
398
  const formattedDate = (0, _datePicker.formatDateInput)(dateStr);
323
- isValidDate(formattedDate) ? onChangeDateRef.current(formattedDate) : onChangeDateRef.current(dateStr);
324
- }, [checkDateViolations, restorePreviousDate]);
399
+ commitDate(isValidDate(formattedDate) ? formattedDate : dateStr);
400
+ }, [commitDate, reportViolationIfAny, restorePreviousDate]);
401
+ /**
402
+ * 타이핑으로 완성된 날짜를 blur 를 기다리지 않고 즉시 부모로 올린다. 통지했으면 true.
403
+ *
404
+ * blur 까지 미루면, 타이핑 중 부모 리렌더가 한 번이라도 끼는 순간 react-flatpickr 의
405
+ * "value !== input.value 면 setDate(value, false)" effect 가 타이핑 값을 부모가 들고 있던
406
+ * 값으로 되돌려 버린다. 그러면 blur 시점에는 바뀐 게 없다고 판정돼 onChangeDate 도,
407
+ * 뒤이은 범위 검증(onDateValidation)도 돌지 않는다. (#410 재보고분)
408
+ */
409
+ const notifyTypedDate = (0, _react.useCallback)((instance, formattedInput) => {
410
+ const typedDate = (0, _moment.default)(formattedInput, (0, _datePicker.convertToMomentFormat)(dateFormatRef.current), true).toDate();
411
+ // min/max 는 instance.config 를 단일 소스로 쓴다. onReady 시점 값을 ref 에 담아두면,
412
+ // 부모가 minDate/maxDate 를 바꿨을 때 타이핑 경로와 blur/캘린더 경로의 판정이 갈릴 수 있다
413
+ const violations = checkDateViolations(typedDate, instance.config.minDate, instance.config.maxDate);
414
+ // 범위 위반은 타이핑 중에 보고하지 않는다 — 한 글자마다 알럿이 뜬다.
415
+ // 입력을 마치고 빠져나갈 때 scheduleBlurSync 의 reportViolationIfAny 가 한 번만 보고한다.
416
+ if (violations.length > 0) return false;
417
+ const dateToNotify = formatSelectedDate(typedDate, dateFormatRef.current);
418
+ // 입력 문자열이 이미 최종 표기와 같을 때만 통지한다. 다르면(예: enableTime 인데 시간을
419
+ // 아직 안 친 상태) 아래 setDate 가 input 값을 다시 써서 타이핑 중 커서가 튄다.
420
+ // 그 경우는 종전대로 blur 시점 동기화에 맡긴다.
421
+ if (dateToNotify === null || dateToNotify !== formattedInput) return false;
422
+ if (isAlreadyNotified(dateToNotify)) return false;
423
+ instance.setDate(typedDate, false);
424
+ return commitDate(dateToNotify);
425
+ }, [checkDateViolations, commitDate, isAlreadyNotified]);
325
426
  /** input에 직접 타이핑할 때 날짜 형식 자동 변환 및 유효성 검사 */
326
427
  const onInputHandler = (0, _react.useCallback)(e => {
327
428
  const target = e.target;
@@ -331,7 +432,7 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
331
432
  if (!input?.trim()) {
332
433
  target.value = '';
333
434
  instance.setDate('', false);
334
- onChangeDateRef.current('');
435
+ commitDate('');
335
436
  return;
336
437
  }
337
438
  if (!/[0-9]/.test(input)) {
@@ -340,21 +441,20 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
340
441
  }
341
442
  const formattedInput = (0, _datePicker.formatDateInput)(input);
342
443
  if (formattedInput !== input) {
444
+ // 여기서 멈추지 않는다. 8자리 숫자를 다 치거나 붙여넣으면 이 시점에 이미 완성된
445
+ // 날짜이고, 우리가 다시 쓴 값에 대해 브라우저는 input 이벤트를 더 주지 않는다.
343
446
  target.value = formattedInput;
344
- return;
345
447
  }
346
- if (!formattedInput || formattedInput.length < 10) return;
347
- const parsedDate = (0, _moment.default)(formattedInput);
348
- if (!parsedDate.isValid()) {
448
+ // 완성 전에는 검증하지 않는다. 중간 상태를 완성으로 오판하면 아래에서 입력이 되돌아간다
449
+ if (formattedInput.length < fullValueLengthRef.current) return;
450
+ // 포맷을 넘겨 strict 로 파싱한다. 포맷 없이 moment() 만 쓰면 시간 전용 모드의
451
+ // '14:30' 같은 값을 파싱하지 못해 정상 입력이 되돌려진다
452
+ if (!(0, _moment.default)(formattedInput, (0, _datePicker.convertToMomentFormat)(dateFormatRef.current), true).isValid()) {
349
453
  restorePreviousDate(target, instance);
350
454
  return;
351
455
  }
352
- const parsedDateObj = parsedDate.toDate();
353
- const violations = checkDateViolations(parsedDateObj, minMaxDateRef.current.minDate, minMaxDateRef.current.maxDate);
354
- if (violations.length > 0) {
355
- return;
356
- }
357
- }, [checkDateViolations, restorePreviousDate]);
456
+ notifyTypedDate(instance, formattedInput);
457
+ }, [commitDate, notifyTypedDate, restorePreviousDate]);
358
458
  /** 시간 입력 필드 - 시(hour) 값 포맷팅 (0~23) */
359
459
  const onHourInputHandler = (0, _react.useCallback)(e => {
360
460
  const target = e.target;
@@ -412,15 +512,16 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
412
512
  if (!input) return;
413
513
  flatpickrInstanceRef.current = instance;
414
514
  dateFormatRef.current = instance.config.dateFormat || 'Y-m-d';
415
- minMaxDateRef.current = {
416
- minDate: instance.config.minDate,
417
- maxDate: instance.config.maxDate
418
- };
419
- // blur 시 현재 날짜를 저장하여 잘못된 입력 시 복원에 사용
515
+ fullValueLengthRef.current = getFullValueLength(dateFormatRef.current);
516
+ // blur 시 현재 날짜를 저장하여 잘못된 입력 시 복원에 사용하고,
517
+ // flatpickr가 조용히 반영한 값이 남아 있으면 부모로 올린다 (scheduleBlurSync 주석 참고)
420
518
  const onBlurHandler = _e => {
519
+ // 아래에서 덮어쓰기 전의 값을 넘긴다. 위반 보고 시 previousDate 로 쓰인다
520
+ const previousDate = instance?._previousDateBeforeInput;
421
521
  if (instance && instance.selectedDates.length > 0) {
422
522
  instance._previousDateBeforeInput = instance.selectedDates[0];
423
523
  }
524
+ scheduleBlurSync(previousDate);
424
525
  };
425
526
  input.addEventListener('input', onInputHandler);
426
527
  input.addEventListener('blur', onBlurHandler);
@@ -444,6 +545,8 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
444
545
  const instance = fpInstance;
445
546
  const input = instance.input;
446
547
  if (!input) return;
548
+ // 파괴 직전에 이 input 이 포커스를 쥐고 있었는지 기억한다 (restoreFocus 주석 참고)
549
+ hadFocusBeforeDestroyRef.current = typeof document !== 'undefined' && document.activeElement === input;
447
550
  flatpickrInstanceRef.current = null;
448
551
  input.removeEventListener('input', onInputHandler);
449
552
  const onBlurHandler = instance._onBlurHandler;
@@ -456,7 +559,7 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
456
559
  ...datePickerOptions,
457
560
  // 어드민 환경 — 모바일에서 native picker 로 fallback 되지 않도록 강제 (datePickerOptions 로 override 불가)
458
561
  disableMobile: true
459
- }), [portal, onChangeDateHandler, onInputHandler, onHourInputHandler, onMinuteInputHandler, datePickerOptions, getPortalContainer]);
562
+ }), [portal, onChangeDateHandler, onInputHandler, onHourInputHandler, onMinuteInputHandler, datePickerOptions, getPortalContainer, scheduleBlurSync]);
460
563
  const iconName = hasTimeOption && datePickerOptions && Object.hasOwn(datePickerOptions, 'noCalendar') ? 'clock' : 'calendar';
461
564
  // ──────────────────────────────────────────────
462
565
  // 시간 옵션에 따른 currentDate 전처리
@@ -471,6 +574,39 @@ const DatePicker = exports.DatePicker = /*#__PURE__*/(0, _react.forwardRef)((_re
471
574
  if (isTimeOnly) return getTimeOnlyDefault(hasSeconds, isEndDate);
472
575
  return getDateTimeDefault(currentDate, hasSeconds, isEndDate);
473
576
  }, [currentDate, isEndDate, options.enableSeconds, options.enableTime, options.noCalendar]);
577
+ // onReady 클로저는 최초 렌더 값에 고정되므로, blur 동기화가 최신 로드값과 비교할 수 있도록 ref에 반영
578
+ if (loadedDateRef.current !== processedCurrentDate) {
579
+ loadedDateRef.current = processedCurrentDate;
580
+ // 부모가 값을 갱신했으면 "이미 통지했다"는 기억은 무효다.
581
+ // 남겨두면, 사용처가 overlap 통지를 받고 날짜를 되돌린 뒤 사용자가 같은 날짜를 다시 입력할 때
582
+ // 중복으로 오판해 통지를 건너뛴다 — 콜백이 첫 회만 오고 그 뒤로 안 오는 증상이 된다
583
+ lastNotifiedDateRef.current = null;
584
+ }
585
+ /**
586
+ * 리렌더로 잃은 포커스를 되돌린다.
587
+ *
588
+ * react-flatpickr 는 렌더마다 flatpickr 인스턴스를 destroy/create 하는데, portal 을 안 쓰면
589
+ * static:true 라 destroy 가 input 을 감싼 .flatpickr-wrapper 를 헐면서 input 노드를 DOM 에서
590
+ * 옮긴다. 포커스를 쥔 노드를 옮기면 포커스가 풀린다.
591
+ *
592
+ * 그래서 사용처가 onDateValidation 을 받고 날짜를 되돌리면, 그 리렌더에서 입력 칸의 포커스가
593
+ * 빠진다. 사용자는 알럿을 닫고 이어서 타이핑하는데 입력이 아무 데도 들어가지 않아,
594
+ * 콜백이 "됐다 안 됐다" 하는 것처럼 보인다. (#416)
595
+ *
596
+ * 자식(Flatpickr)의 effect 가 먼저 실행되므로 여기서 마지막에 되돌린다.
597
+ * 포커스가 복원되면 flatpickr 가 캘린더를 여는데, 이는 사용자가 칸을 직접 클릭했을 때와 같은
598
+ * 상태이므로 그대로 둔다.
599
+ */
600
+ // 의도적으로 deps 없음 — 자식 Flatpickr effect 가 인스턴스를 재생성한 뒤에 실행되어야 한다
601
+ (0, _react.useEffect)(() => {
602
+ if (!hadFocusBeforeDestroyRef.current) return;
603
+ hadFocusBeforeDestroyRef.current = false;
604
+ const input = flatpickrInstanceRef.current?.input;
605
+ if (!input || document.activeElement === input) return;
606
+ input.focus({
607
+ preventScroll: true
608
+ });
609
+ });
474
610
  // ──────────────────────────────────────────────
475
611
  // 렌더링
476
612
  // ──────────────────────────────────────────────