@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.
- package/dist/cjs/src/components/forms-and-input/checkbox/CheckboxInput.js +3 -2
- package/dist/cjs/src/components/forms-and-input/date-picker/DatePicker.js +17 -20
- package/dist/cjs/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.js +53 -0
- package/dist/cjs/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.js +129 -0
- 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/select/Select.js +12 -1
- package/dist/cjs/src/components/forms-and-input/select-box/SelectBox.js +5 -2
- 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/cjs/src/utils/__tests__/date-picker.test.js +29 -0
- package/dist/cjs/src/utils/date-picker.js +15 -1
- package/dist/esm/src/components/forms-and-input/checkbox/CheckboxInput.js +4 -3
- package/dist/esm/src/components/forms-and-input/date-picker/DatePicker.js +18 -21
- package/dist/esm/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.js +50 -0
- package/dist/esm/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.js +126 -0
- 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/select/Select.js +12 -1
- package/dist/esm/src/components/forms-and-input/select-box/SelectBox.js +5 -2
- 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/esm/src/utils/__tests__/date-picker.test.js +26 -0
- package/dist/esm/src/utils/date-picker.js +13 -0
- package/dist/temp/src/components/forms-and-input/checkbox/CheckboxInput.js +4 -3
- package/dist/temp/src/components/forms-and-input/date-picker/DatePicker.js +18 -28
- package/dist/temp/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.d.ts +1 -0
- package/dist/temp/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.js +43 -0
- package/dist/temp/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.d.ts +1 -0
- package/dist/temp/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.js +109 -0
- 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/select/Select.d.ts +1 -0
- package/dist/temp/src/components/forms-and-input/select/Select.js +10 -2
- package/dist/temp/src/components/forms-and-input/select-box/SelectBox.d.ts +1 -0
- package/dist/temp/src/components/forms-and-input/select-box/SelectBox.js +5 -4
- 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/temp/src/utils/__tests__/date-picker.test.d.ts +1 -0
- package/dist/temp/src/utils/__tests__/date-picker.test.js +26 -0
- package/dist/temp/src/utils/date-picker.d.ts +2 -0
- package/dist/temp/src/utils/date-picker.js +13 -0
- package/dist/types/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.d.ts +1 -0
- package/dist/types/src/components/forms-and-input/date-picker/__tests__/DatePicker.test.d.ts +1 -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/forms-and-input/select/Select.d.ts +1 -0
- package/dist/types/src/components/forms-and-input/select-box/SelectBox.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/types/src/utils/__tests__/date-picker.test.d.ts +1 -0
- package/dist/types/src/utils/date-picker.d.ts +2 -0
- package/dist/ui-admin/assets/styles/style.css +33 -21
- package/package.json +2 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { createElement } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { act } from 'react-dom/test-utils';
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { Pagination } from '../Pagination';
|
|
7
|
+
const ITEM_COUNT_PER_PAGE = 10;
|
|
8
|
+
const PAGE_GROUP_SIZE = 10;
|
|
9
|
+
const TOTAL_COUNT_100_PAGES = 1000;
|
|
10
|
+
const TOTAL_COUNT_50_PAGES = 500;
|
|
11
|
+
const CURRENT_PAGE = 91; // 100페이지 기준 마지막 그룹(91~100)을 보던 페이지
|
|
12
|
+
const GROUP_START_100_PAGES = 91; // currentPage=91 일 때의 그룹 시작
|
|
13
|
+
const GROUP_START_50_PAGES = 41; // totalPage=50 으로 줄었을 때의 마지막 그룹 시작
|
|
14
|
+
const LAST_PAGE_50 = 50;
|
|
15
|
+
let root = null;
|
|
16
|
+
let container = null;
|
|
17
|
+
function render(props) {
|
|
18
|
+
if (!container) {
|
|
19
|
+
container = document.createElement('div');
|
|
20
|
+
document.body.appendChild(container);
|
|
21
|
+
root = createRoot(container);
|
|
22
|
+
}
|
|
23
|
+
act(() => {
|
|
24
|
+
root?.render(createElement(Pagination, props));
|
|
25
|
+
});
|
|
26
|
+
return container;
|
|
27
|
+
}
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
if (root && container) {
|
|
30
|
+
act(() => root?.unmount());
|
|
31
|
+
container.remove();
|
|
32
|
+
}
|
|
33
|
+
root = null;
|
|
34
|
+
container = null;
|
|
35
|
+
});
|
|
36
|
+
const pageNums = (el) => Array.from(el.querySelectorAll('.ncua-pagination__page-num')).map((b) => Number(b.textContent));
|
|
37
|
+
describe('#332 Pagination — totalPage 축소 시 start 보정', () => {
|
|
38
|
+
it('totalPage 가 줄면 start 를 마지막 그룹으로 보정하고 범위 밖 페이지 버튼을 표시하지 않는다', () => {
|
|
39
|
+
// 1) 100 페이지에서 91 페이지 보는 중 → 91~100 그룹 표시
|
|
40
|
+
const el = render({
|
|
41
|
+
totalCount: TOTAL_COUNT_100_PAGES,
|
|
42
|
+
itemCountPerPage: ITEM_COUNT_PER_PAGE,
|
|
43
|
+
pageCount: PAGE_GROUP_SIZE,
|
|
44
|
+
currentPage: CURRENT_PAGE,
|
|
45
|
+
});
|
|
46
|
+
expect(pageNums(el)[0]).toBe(GROUP_START_100_PAGES);
|
|
47
|
+
// 2) 데이터가 50 페이지로 줄어듦 (currentPage 는 그대로 — 부모가 clamp 안 한 상황)
|
|
48
|
+
render({
|
|
49
|
+
totalCount: TOTAL_COUNT_50_PAGES,
|
|
50
|
+
itemCountPerPage: ITEM_COUNT_PER_PAGE,
|
|
51
|
+
pageCount: PAGE_GROUP_SIZE,
|
|
52
|
+
currentPage: CURRENT_PAGE,
|
|
53
|
+
});
|
|
54
|
+
const nums = pageNums(el);
|
|
55
|
+
// 마지막 그룹(41~50)으로 보정되고, totalPage(50)를 넘는 버튼은 없어야 한다
|
|
56
|
+
expect(nums[0]).toBe(GROUP_START_50_PAGES);
|
|
57
|
+
expect(Math.max(...nums)).toBeLessThanOrEqual(LAST_PAGE_50);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -4,6 +4,7 @@ import classNames from 'classnames';
|
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
6
6
|
import { ButtonCloseX } from '../../action/button/ButtonCloseX';
|
|
7
|
+
import { calculateArrowOffsetX } from './utils';
|
|
7
8
|
const ICON_SIZE_SM = 14;
|
|
8
9
|
const ICON_SIZE_DEFAULT = 16;
|
|
9
10
|
// HTML 구현체(assets/scripts/tooltip)와 동일한 상수
|
|
@@ -72,13 +73,25 @@ const computePanelCoords = (prefer, anchor, panel) => {
|
|
|
72
73
|
}
|
|
73
74
|
left = Math.max(VIEWPORT_MARGIN, Math.min(left, window.innerWidth - panel.width - VIEWPORT_MARGIN));
|
|
74
75
|
top = Math.max(VIEWPORT_MARGIN, Math.min(top, window.innerHeight - panel.height - VIEWPORT_MARGIN));
|
|
75
|
-
|
|
76
|
+
// 경계 보정으로 패널이 앵커 중심에서 밀렸을 때도 화살표(::after)가 앵커를 가리키도록
|
|
77
|
+
// 패널 내 X 오프셋 계산 (top/bottom 계열만 — SCSS의 --top/--bottom 규칙이 소비)
|
|
78
|
+
let arrowX = null;
|
|
79
|
+
if (calculatedPosition === 'top' || calculatedPosition === 'bottom') {
|
|
80
|
+
arrowX = calculateArrowOffsetX(anchor.left + anchor.width / 2, left, panel.width);
|
|
81
|
+
}
|
|
82
|
+
return { top: Math.round(top), left: Math.round(left), arrowX, calculatedPosition };
|
|
76
83
|
};
|
|
77
84
|
const buildPanelStyle = (disablePortal, coords, visible, zIndex) => {
|
|
78
85
|
const opacity = visible ? 1 : 0;
|
|
79
86
|
if (disablePortal)
|
|
80
87
|
return { opacity };
|
|
81
|
-
return {
|
|
88
|
+
return {
|
|
89
|
+
top: `${coords.top}px`,
|
|
90
|
+
left: `${coords.left}px`,
|
|
91
|
+
opacity,
|
|
92
|
+
...(coords.arrowX !== null && { '--ncua-tooltip-arrow-x': `${coords.arrowX}px` }),
|
|
93
|
+
...(zIndex && { zIndex }),
|
|
94
|
+
};
|
|
82
95
|
};
|
|
83
96
|
const renderIcon = (iconStyle, iconType, iconSize, iconColor) => {
|
|
84
97
|
if (iconStyle === 'help-circle') {
|
|
@@ -92,7 +105,11 @@ export const Tooltip = ({ tooltipType = 'white', iconType = 'stroke', position =
|
|
|
92
105
|
const panelRef = useRef(null);
|
|
93
106
|
const rafIdRef = useRef(null);
|
|
94
107
|
const [mounted, setMounted] = useState(false);
|
|
95
|
-
const [coords, setCoords] = useState({
|
|
108
|
+
const [coords, setCoords] = useState({
|
|
109
|
+
top: OFFSCREEN,
|
|
110
|
+
left: OFFSCREEN,
|
|
111
|
+
arrowX: null,
|
|
112
|
+
});
|
|
96
113
|
const [calculatedPosition, setCalculatedPosition] = useState(position === 'auto' ? 'bottom' : position);
|
|
97
114
|
const [isVisible, setIsVisible] = useState(false);
|
|
98
115
|
const [isManuallyClose, setIsManuallyClose] = useState(false);
|
|
@@ -112,7 +129,7 @@ export const Tooltip = ({ tooltipType = 'white', iconType = 'stroke', position =
|
|
|
112
129
|
const anchor = anchorRef.current.getBoundingClientRect();
|
|
113
130
|
const panel = panelRef.current.getBoundingClientRect();
|
|
114
131
|
const next = computePanelCoords(position, anchor, panel);
|
|
115
|
-
setCoords({ top: next.top, left: next.left });
|
|
132
|
+
setCoords({ top: next.top, left: next.left, arrowX: next.arrowX });
|
|
116
133
|
setCalculatedPosition(next.calculatedPosition);
|
|
117
134
|
}, [position, disablePortal]);
|
|
118
135
|
useEffect(() => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ARROW_EDGE_MARGIN, calculateArrowOffsetX } from '../utils';
|
|
3
|
+
// 공통 픽스처 — 너비 300px 패널이 viewport 왼쪽 여백(8px)에 클램핑된 상태를 가정
|
|
4
|
+
const PANEL_WIDTH = 300;
|
|
5
|
+
const CLAMPED_LEFT = 8;
|
|
6
|
+
describe('calculateArrowOffsetX — 화살표가 앵커 중심을 가리키도록 패널 내 X 오프셋 계산', () => {
|
|
7
|
+
it('패널이 클램핑되지 않으면(앵커 중심 = 패널 중심) 패널 너비의 절반을 반환한다', () => {
|
|
8
|
+
const anchorCenterX = 200;
|
|
9
|
+
const centeredPanelLeft = 50;
|
|
10
|
+
const panelHalfWidth = 150;
|
|
11
|
+
expect(calculateArrowOffsetX(anchorCenterX, centeredPanelLeft, PANEL_WIDTH)).toBe(panelHalfWidth);
|
|
12
|
+
});
|
|
13
|
+
it('패널이 오른쪽으로 밀린 경우(왼쪽 경계 클램핑) 화살표는 왼쪽으로 이동해 앵커를 가리킨다', () => {
|
|
14
|
+
const anchorCenterX = 60;
|
|
15
|
+
const expectedOffset = 52; // 60 - 8
|
|
16
|
+
expect(calculateArrowOffsetX(anchorCenterX, CLAMPED_LEFT, PANEL_WIDTH)).toBe(expectedOffset);
|
|
17
|
+
});
|
|
18
|
+
it('패널이 왼쪽으로 밀린 경우(오른쪽 경계 클램핑) 화살표는 오른쪽으로 이동해 앵커를 가리킨다', () => {
|
|
19
|
+
const anchorCenterX = 950;
|
|
20
|
+
const clampedRightPanelLeft = 692;
|
|
21
|
+
const expectedOffset = 258; // 950 - 692
|
|
22
|
+
expect(calculateArrowOffsetX(anchorCenterX, clampedRightPanelLeft, PANEL_WIDTH)).toBe(expectedOffset);
|
|
23
|
+
});
|
|
24
|
+
it('앵커가 패널 왼쪽 모서리보다 바깥이면 최소 여백(모서리 radius + 화살표)으로 클램핑한다', () => {
|
|
25
|
+
const outsideLeftAnchorX = 10;
|
|
26
|
+
expect(calculateArrowOffsetX(outsideLeftAnchorX, CLAMPED_LEFT, PANEL_WIDTH)).toBe(ARROW_EDGE_MARGIN);
|
|
27
|
+
});
|
|
28
|
+
it('앵커가 패널 오른쪽 모서리보다 바깥이면 최대 여백으로 클램핑한다', () => {
|
|
29
|
+
const outsideRightAnchorX = 999;
|
|
30
|
+
expect(calculateArrowOffsetX(outsideRightAnchorX, CLAMPED_LEFT, PANEL_WIDTH)).toBe(PANEL_WIDTH - ARROW_EDGE_MARGIN);
|
|
31
|
+
});
|
|
32
|
+
it('소수 좌표는 반올림한다', () => {
|
|
33
|
+
const fractionalAnchorX = 100.4;
|
|
34
|
+
const panelLeft = 50;
|
|
35
|
+
const expectedRounded = 50; // 100.4 - 50 = 50.4 → 50
|
|
36
|
+
expect(calculateArrowOffsetX(fractionalAnchorX, panelLeft, PANEL_WIDTH)).toBe(expectedRounded);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const ARROW_EDGE_MARGIN = 12;
|
|
2
|
+
/**
|
|
3
|
+
* top/bottom 툴팁 화살표(::after)의 패널 내 X 오프셋.
|
|
4
|
+
* 패널이 viewport 경계 보정으로 앵커 중심에서 밀려도 화살표는 앵커를 가리키도록,
|
|
5
|
+
* 클램핑된 패널 left 기준으로 앵커 중심까지의 거리를 계산한다.
|
|
6
|
+
* 앵커가 패널 밖에 있으면 패널 모서리 여백까지만 이동한다.
|
|
7
|
+
* React(Tooltip.tsx)와 HTML 구현체(assets/scripts/tooltip)가 공유한다.
|
|
8
|
+
*/
|
|
9
|
+
export declare function calculateArrowOffsetX(anchorCenterX: number, panelLeft: number, panelWidth: number): number;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// 화살표가 패널 모서리(border-radius 6px + 화살표 반너비 6px) 안쪽으로 파고들지 않기 위한 최소 여백
|
|
2
|
+
export const ARROW_EDGE_MARGIN = 12;
|
|
3
|
+
/**
|
|
4
|
+
* top/bottom 툴팁 화살표(::after)의 패널 내 X 오프셋.
|
|
5
|
+
* 패널이 viewport 경계 보정으로 앵커 중심에서 밀려도 화살표는 앵커를 가리키도록,
|
|
6
|
+
* 클램핑된 패널 left 기준으로 앵커 중심까지의 거리를 계산한다.
|
|
7
|
+
* 앵커가 패널 밖에 있으면 패널 모서리 여백까지만 이동한다.
|
|
8
|
+
* React(Tooltip.tsx)와 HTML 구현체(assets/scripts/tooltip)가 공유한다.
|
|
9
|
+
*/
|
|
10
|
+
export function calculateArrowOffsetX(anchorCenterX, panelLeft, panelWidth) {
|
|
11
|
+
const raw = anchorCenterX - panelLeft;
|
|
12
|
+
return Math.round(Math.max(ARROW_EDGE_MARGIN, Math.min(raw, panelWidth - ARROW_EDGE_MARGIN)));
|
|
13
|
+
}
|
|
@@ -14,6 +14,9 @@ export const useMediaQuery = (query, options) => {
|
|
|
14
14
|
return () => {
|
|
15
15
|
mediaQuery.removeEventListener('change', handleChange);
|
|
16
16
|
};
|
|
17
|
+
// options 객체 전체가 아니라 실제 사용하는 onMatched 콜백에만 의존한다. options 를 통째로
|
|
18
|
+
// 넣으면 매 렌더 새 객체로 간주돼 불필요하게 재구독한다.
|
|
19
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
17
20
|
}, [query, options?.onMatched]);
|
|
18
21
|
return matches;
|
|
19
22
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import moment from 'moment';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { convertToMomentFormat } from '../date-picker';
|
|
4
|
+
describe('convertToMomentFormat', () => {
|
|
5
|
+
it('날짜 전용 포맷을 moment 토큰으로 변환한다 (기존 동작 보존)', () => {
|
|
6
|
+
expect(convertToMomentFormat('Y-m-d')).toBe('YYYY-MM-DD');
|
|
7
|
+
});
|
|
8
|
+
it('2자리 연도(y)를 YY로 변환한다', () => {
|
|
9
|
+
expect(convertToMomentFormat('y-m-d')).toBe('YY-MM-DD');
|
|
10
|
+
});
|
|
11
|
+
it('시간 포함 포맷의 H/i/S를 모두 변환한다 (버그 수정 지점)', () => {
|
|
12
|
+
expect(convertToMomentFormat('Y-m-d H:i')).toBe('YYYY-MM-DD HH:mm');
|
|
13
|
+
expect(convertToMomentFormat('Y-m-d H:i:S')).toBe('YYYY-MM-DD HH:mm:ss');
|
|
14
|
+
});
|
|
15
|
+
it('월(m)과 분(i)이 함께 있어도 서로 침범하지 않는다', () => {
|
|
16
|
+
// m -> MM(월) 먼저, i -> mm(분) 나중 순서가 지켜져야 함
|
|
17
|
+
expect(convertToMomentFormat('Y-m-d H:i')).not.toContain('i');
|
|
18
|
+
expect(convertToMomentFormat('Y-m-d H:i')).toBe('YYYY-MM-DD HH:mm');
|
|
19
|
+
});
|
|
20
|
+
it('실제 moment 출력이 의도한 문자열과 일치한다 (회귀 핵심)', () => {
|
|
21
|
+
const date = moment('2024-03-20 14:30:05', 'YYYY-MM-DD HH:mm:ss').toDate();
|
|
22
|
+
expect(moment(date).format(convertToMomentFormat('Y-m-d'))).toBe('2024-03-20');
|
|
23
|
+
expect(moment(date).format(convertToMomentFormat('Y-m-d H:i'))).toBe('2024-03-20 14:30');
|
|
24
|
+
expect(moment(date).format(convertToMomentFormat('Y-m-d H:i:S'))).toBe('2024-03-20 14:30:05');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -6,6 +6,8 @@ export declare function getSubtractDate({ date, period, unit, }: {
|
|
|
6
6
|
period: DurationInputArg1;
|
|
7
7
|
unit: DurationInputArg2;
|
|
8
8
|
}): Date;
|
|
9
|
+
/** flatpickr dateFormat 토큰을 moment 포맷 토큰으로 변환 (단일 패스 치환으로 토큰 간 간섭 방지) */
|
|
10
|
+
export declare const convertToMomentFormat: (format: string) => string;
|
|
9
11
|
export declare const formatDateInput: (input: string) => string;
|
|
10
12
|
export declare const formatHourInput: (input: string) => string;
|
|
11
13
|
export declare const formatMinuteInput: (input: string) => string;
|
|
@@ -15,6 +15,19 @@ export function getToday() {
|
|
|
15
15
|
export function getSubtractDate({ date = getToday(), period, unit, }) {
|
|
16
16
|
return moment(date).subtract(period, unit).toDate();
|
|
17
17
|
}
|
|
18
|
+
/** flatpickr dateFormat 토큰을 moment 포맷 토큰으로 변환 (단일 패스 치환으로 토큰 간 간섭 방지) */
|
|
19
|
+
export const convertToMomentFormat = (format) => {
|
|
20
|
+
const tokenMap = {
|
|
21
|
+
Y: 'YYYY',
|
|
22
|
+
y: 'YY',
|
|
23
|
+
m: 'MM',
|
|
24
|
+
d: 'DD',
|
|
25
|
+
H: 'HH',
|
|
26
|
+
i: 'mm',
|
|
27
|
+
S: 'ss',
|
|
28
|
+
};
|
|
29
|
+
return format.replace(/[YymdHiS]/g, (matched) => tokenMap[matched] || matched);
|
|
30
|
+
};
|
|
18
31
|
export const formatDateInput = (input) => {
|
|
19
32
|
const dateRegex = /^(19|20)\d{2}(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[0-1])$/;
|
|
20
33
|
if (dateRegex.test(input)) {
|
package/dist/types/src/components/forms-and-input/date-picker/__tests__/DatePicker.memo.test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -7,6 +7,7 @@ export interface SelectProps extends Omit<ComponentPropsWithRef<'select'>, 'size
|
|
|
7
7
|
disabledPlaceholder?: boolean;
|
|
8
8
|
hintText?: string;
|
|
9
9
|
destructive?: boolean;
|
|
10
|
+
readOnly?: boolean;
|
|
10
11
|
size?: Extract<Size, 'xs' | 'sm' | 'md'>;
|
|
11
12
|
optionItems?: OptionType[];
|
|
12
13
|
register?: UseFormRegisterReturn;
|
|
@@ -16,6 +16,7 @@ type SelectBoxProps = Omit<ComponentPropsWithRef<'div'>, 'size' | 'onChange'> &
|
|
|
16
16
|
value?: OptionValue;
|
|
17
17
|
onChange?: OptionChangeHandler;
|
|
18
18
|
disabled?: boolean;
|
|
19
|
+
readOnly?: boolean;
|
|
19
20
|
register?: UseFormRegisterReturn;
|
|
20
21
|
maxHeight?: number;
|
|
21
22
|
multiple?: boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const ARROW_EDGE_MARGIN = 12;
|
|
2
|
+
/**
|
|
3
|
+
* top/bottom 툴팁 화살표(::after)의 패널 내 X 오프셋.
|
|
4
|
+
* 패널이 viewport 경계 보정으로 앵커 중심에서 밀려도 화살표는 앵커를 가리키도록,
|
|
5
|
+
* 클램핑된 패널 left 기준으로 앵커 중심까지의 거리를 계산한다.
|
|
6
|
+
* 앵커가 패널 밖에 있으면 패널 모서리 여백까지만 이동한다.
|
|
7
|
+
* React(Tooltip.tsx)와 HTML 구현체(assets/scripts/tooltip)가 공유한다.
|
|
8
|
+
*/
|
|
9
|
+
export declare function calculateArrowOffsetX(anchorCenterX: number, panelLeft: number, panelWidth: number): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -6,6 +6,8 @@ export declare function getSubtractDate({ date, period, unit, }: {
|
|
|
6
6
|
period: DurationInputArg1;
|
|
7
7
|
unit: DurationInputArg2;
|
|
8
8
|
}): Date;
|
|
9
|
+
/** flatpickr dateFormat 토큰을 moment 포맷 토큰으로 변환 (단일 패스 치환으로 토큰 간 간섭 방지) */
|
|
10
|
+
export declare const convertToMomentFormat: (format: string) => string;
|
|
9
11
|
export declare const formatDateInput: (input: string) => string;
|
|
10
12
|
export declare const formatHourInput: (input: string) => string;
|
|
11
13
|
export declare const formatMinuteInput: (input: string) => string;
|
|
@@ -3202,7 +3202,7 @@ button {
|
|
|
3202
3202
|
transform: translateX(50%);
|
|
3203
3203
|
}
|
|
3204
3204
|
.ncua-tooltip__bg--top::after {
|
|
3205
|
-
left: 50
|
|
3205
|
+
left: var(--ncua-tooltip-arrow-x, 50%);
|
|
3206
3206
|
transform: translateX(-50%);
|
|
3207
3207
|
}
|
|
3208
3208
|
.ncua-tooltip__bg--top-left {
|
|
@@ -3248,7 +3248,7 @@ button {
|
|
|
3248
3248
|
transform: translateX(50%);
|
|
3249
3249
|
}
|
|
3250
3250
|
.ncua-tooltip__bg--bottom::after {
|
|
3251
|
-
left: 50
|
|
3251
|
+
left: var(--ncua-tooltip-arrow-x, 50%);
|
|
3252
3252
|
transform: translateX(-50%);
|
|
3253
3253
|
}
|
|
3254
3254
|
.ncua-tooltip__bg--bottom-left {
|
|
@@ -3415,6 +3415,12 @@ button {
|
|
|
3415
3415
|
.ncua-select__content:has(.ncua-select__tag:disabled) {
|
|
3416
3416
|
border-color: var(--gray-200);
|
|
3417
3417
|
}
|
|
3418
|
+
.ncua-select--readonly .ncua-select__tag {
|
|
3419
|
+
pointer-events: none;
|
|
3420
|
+
background-color: var(--gray-50);
|
|
3421
|
+
color: var(--gray-700);
|
|
3422
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14' fill='none'%3E%3Cpath d='M3.5 5.25L7 8.75L10.5 5.25' stroke='%23a4a5a8' stroke-width='1.16667' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
3423
|
+
}
|
|
3418
3424
|
.ncua-select.destructive .ncua-select__content {
|
|
3419
3425
|
border-color: var(--primary-red-600);
|
|
3420
3426
|
}
|
|
@@ -3644,7 +3650,7 @@ button {
|
|
|
3644
3650
|
cursor: pointer;
|
|
3645
3651
|
transition: all 0.15s ease-in-out;
|
|
3646
3652
|
}
|
|
3647
|
-
.ncua-selectbox__content:hover:not(.ncua-selectbox--disabled .ncua-selectbox__content) {
|
|
3653
|
+
.ncua-selectbox__content:hover:not(.ncua-selectbox--disabled .ncua-selectbox__content):not(.ncua-selectbox--readonly .ncua-selectbox__content) {
|
|
3648
3654
|
border-color: var(--gray-300);
|
|
3649
3655
|
}
|
|
3650
3656
|
.ncua-selectbox__content:focus {
|
|
@@ -3727,6 +3733,16 @@ button {
|
|
|
3727
3733
|
.ncua-selectbox--disabled .ncua-selectbox__arrow {
|
|
3728
3734
|
color: var(--gray-300);
|
|
3729
3735
|
}
|
|
3736
|
+
.ncua-selectbox--readonly .ncua-selectbox__content {
|
|
3737
|
+
background-color: var(--gray-50);
|
|
3738
|
+
cursor: default;
|
|
3739
|
+
}
|
|
3740
|
+
.ncua-selectbox--readonly .ncua-selectbox__value {
|
|
3741
|
+
color: var(--gray-700);
|
|
3742
|
+
}
|
|
3743
|
+
.ncua-selectbox--readonly .ncua-selectbox__arrow {
|
|
3744
|
+
color: var(--gray-300);
|
|
3745
|
+
}
|
|
3730
3746
|
.ncua-selectbox.destructive .ncua-selectbox__content {
|
|
3731
3747
|
border-color: var(--primary-red-600);
|
|
3732
3748
|
}
|
|
@@ -3791,7 +3807,7 @@ button {
|
|
|
3791
3807
|
border: 0;
|
|
3792
3808
|
background-color: transparent;
|
|
3793
3809
|
}
|
|
3794
|
-
.ncua-selectbox--simple .ncua-selectbox__content:hover:not(.ncua-selectbox--disabled .ncua-selectbox--simple .ncua-selectbox__content) {
|
|
3810
|
+
.ncua-selectbox--simple .ncua-selectbox__content:hover:not(.ncua-selectbox--disabled .ncua-selectbox--simple .ncua-selectbox__content):not(.ncua-selectbox--readonly .ncua-selectbox--simple .ncua-selectbox__content) {
|
|
3795
3811
|
border-color: transparent;
|
|
3796
3812
|
}
|
|
3797
3813
|
.ncua-selectbox--simple .ncua-selectbox__content:focus {
|
|
@@ -5908,19 +5924,12 @@ button {
|
|
|
5908
5924
|
left: 50%;
|
|
5909
5925
|
transform: translate(-50%, -50%);
|
|
5910
5926
|
z-index: var(--z-index-sticky);
|
|
5927
|
+
margin: 0 !important;
|
|
5911
5928
|
}
|
|
5912
5929
|
.ncua-image-file-input__empty-slot-wrapper .ncua-tooltip svg {
|
|
5913
5930
|
opacity: 0;
|
|
5914
5931
|
cursor: pointer;
|
|
5915
5932
|
}
|
|
5916
|
-
.ncua-image-file-input__empty-slot-wrapper .ncua-tooltip .ncua-tooltip__bg {
|
|
5917
|
-
left: 50%;
|
|
5918
|
-
right: auto;
|
|
5919
|
-
transform: translateX(-50%);
|
|
5920
|
-
width: max-content;
|
|
5921
|
-
max-width: 320px;
|
|
5922
|
-
white-space: nowrap;
|
|
5923
|
-
}
|
|
5924
5933
|
.ncua-image-file-input__preview-container {
|
|
5925
5934
|
position: relative;
|
|
5926
5935
|
border: 1px dashed var(--gray-200);
|
|
@@ -6027,10 +6036,6 @@ button {
|
|
|
6027
6036
|
.ncua-image-file-input--xs .ncua-image-file-input__preview-container:not(:has(.ncua-image-file-input__preview-image))[disabled], .ncua-image-file-input--xs .ncua-image-file-input__preview-container:not(:has(.ncua-image-file-input__preview-image)).is-disable {
|
|
6028
6037
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M10 4.16669V15.8334M4.16669 10H15.8334' stroke='%23d3d4d8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
6029
6038
|
}
|
|
6030
|
-
.ncua-image-file-input--xs .ncua-image-file-input__empty-slot-wrapper .ncua-tooltip .ncua-tooltip__bg {
|
|
6031
|
-
top: auto;
|
|
6032
|
-
bottom: -20px;
|
|
6033
|
-
}
|
|
6034
6039
|
.ncua-image-file-input--sm {
|
|
6035
6040
|
font-size: var(--font-size-sm);
|
|
6036
6041
|
line-height: var(--line-heights-sm);
|
|
@@ -6051,10 +6056,6 @@ button {
|
|
|
6051
6056
|
.ncua-image-file-input--sm .ncua-image-file-input__preview-container:not(:has(.ncua-image-file-input__preview-image))[disabled], .ncua-image-file-input--sm .ncua-image-file-input__preview-container:not(:has(.ncua-image-file-input__preview-image)).is-disable {
|
|
6052
6057
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M10 4.16669V15.8334M4.16669 10H15.8334' stroke='%23d3d4d8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
6053
6058
|
}
|
|
6054
|
-
.ncua-image-file-input--sm .ncua-image-file-input__empty-slot-wrapper .ncua-tooltip .ncua-tooltip__bg {
|
|
6055
|
-
top: auto;
|
|
6056
|
-
bottom: -16px;
|
|
6057
|
-
}
|
|
6058
6059
|
.ncua-image-file-input--xs .ncua-file-input {
|
|
6059
6060
|
gap: 8px;
|
|
6060
6061
|
}
|
|
@@ -6062,6 +6063,10 @@ button {
|
|
|
6062
6063
|
gap: 16px;
|
|
6063
6064
|
}
|
|
6064
6065
|
|
|
6066
|
+
.ncua-tooltip-panel.ncua-tooltip__bg.ncua-image-file-input__slot-tooltip {
|
|
6067
|
+
pointer-events: none;
|
|
6068
|
+
}
|
|
6069
|
+
|
|
6065
6070
|
.ncua-tooltip-panel.ncua-image-file-input__slot-tooltip--xs {
|
|
6066
6071
|
transform: translateY(-19px);
|
|
6067
6072
|
}
|
|
@@ -7029,11 +7034,18 @@ button {
|
|
|
7029
7034
|
color: var(--primary-red-600);
|
|
7030
7035
|
font-weight: 400;
|
|
7031
7036
|
}
|
|
7037
|
+
.ncua-sub-title--middle {
|
|
7038
|
+
gap: 2px;
|
|
7039
|
+
}
|
|
7032
7040
|
.ncua-sub-title--middle .ncua-sub-title__title {
|
|
7033
7041
|
font-size: var(--font-size-md);
|
|
7034
|
-
font-weight:
|
|
7042
|
+
font-weight: var(--font-weights-commerce-sans-1);
|
|
7035
7043
|
line-height: var(--line-heights-md);
|
|
7036
7044
|
}
|
|
7045
|
+
.ncua-sub-title--middle .ncua-sub-title__description {
|
|
7046
|
+
font-size: var(--font-size-xs);
|
|
7047
|
+
line-height: var(--line-heights-xs);
|
|
7048
|
+
}
|
|
7037
7049
|
.ncua-sub-title--sm .ncua-sub-title__title-area {
|
|
7038
7050
|
gap: 6px;
|
|
7039
7051
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ncds/ui-admin",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.15",
|
|
4
4
|
"description": "nhn-commerce의 어드민 디자인 시스템입니다.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"barrel": "node barrel.js",
|
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
"eslint-config-react-app": "7.0.1",
|
|
106
106
|
"file-loader": "6.2.0",
|
|
107
107
|
"html-webpack-plugin": "5.6.6",
|
|
108
|
+
"jsdom": "26.1.0",
|
|
108
109
|
"mini-css-extract-plugin": "2.10.1",
|
|
109
110
|
"prop-types": "15.8.1",
|
|
110
111
|
"react": "18.2.0",
|