@ncds/ui-admin 1.8.21-alpha.8 → 1.8.21
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/feedback-and-status/badge/Badge.js +1 -1
- package/dist/cjs/src/components/forms-and-input/input-base/InputBase.js +13 -7
- package/dist/cjs/src/components/forms-and-input/input-base/__tests__/InputBase.test.js +16 -11
- package/dist/cjs/src/components/navigation/pagination/Pagination.js +3 -1
- package/dist/cjs/src/components/navigation/pagination/__tests__/Pagination.test.js +74 -0
- package/dist/cjs/src/components/overlays/notification/CalloutNotification.js +6 -1
- package/dist/cjs/src/components/overlays/notification/Notification.js +22 -2
- package/dist/cjs/src/components/overlays/notification/__tests__/CalloutNotification.test.js +128 -0
- package/dist/cjs/src/components/overlays/notification/__tests__/Notification.test.js +83 -0
- package/dist/cjs/src/components/overlays/postcode-search-modal/PostcodeSearchModal.js +72 -90
- package/dist/cjs/src/components/overlays/postcode-search-modal/PostcodeSearchModalTip.js +70 -0
- package/dist/cjs/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.js +96 -39
- package/dist/cjs/src/generated/scoped-css.js +1 -1
- package/dist/esm/src/components/feedback-and-status/badge/Badge.js +1 -1
- package/dist/esm/src/components/forms-and-input/input-base/InputBase.js +13 -7
- package/dist/esm/src/components/forms-and-input/input-base/__tests__/InputBase.test.js +16 -11
- package/dist/esm/src/components/navigation/pagination/Pagination.js +3 -1
- package/dist/esm/src/components/navigation/pagination/__tests__/Pagination.test.js +74 -0
- package/dist/esm/src/components/overlays/notification/CalloutNotification.js +6 -1
- package/dist/esm/src/components/overlays/notification/Notification.js +22 -2
- package/dist/esm/src/components/overlays/notification/__tests__/CalloutNotification.test.js +125 -0
- package/dist/esm/src/components/overlays/notification/__tests__/Notification.test.js +80 -0
- package/dist/esm/src/components/overlays/postcode-search-modal/PostcodeSearchModal.js +72 -90
- package/dist/esm/src/components/overlays/postcode-search-modal/PostcodeSearchModalTip.js +63 -0
- package/dist/esm/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.js +96 -39
- package/dist/esm/src/generated/scoped-css.js +1 -1
- package/dist/temp/src/components/feedback-and-status/badge/Badge.d.ts +1 -1
- package/dist/temp/src/components/feedback-and-status/badge/Badge.js +1 -1
- package/dist/temp/src/components/forms-and-input/input-base/InputBase.js +10 -4
- package/dist/temp/src/components/forms-and-input/input-base/__tests__/InputBase.test.js +14 -12
- package/dist/temp/src/components/navigation/pagination/Pagination.js +3 -1
- package/dist/temp/src/components/navigation/pagination/__tests__/Pagination.test.js +74 -0
- package/dist/temp/src/components/overlays/notification/CalloutNotification.d.ts +3 -1
- package/dist/temp/src/components/overlays/notification/CalloutNotification.js +5 -1
- package/dist/temp/src/components/overlays/notification/Notification.d.ts +12 -2
- package/dist/temp/src/components/overlays/notification/Notification.js +9 -4
- package/dist/temp/src/components/overlays/notification/__tests__/CalloutNotification.test.d.ts +1 -0
- package/dist/temp/src/components/overlays/notification/__tests__/CalloutNotification.test.js +108 -0
- package/dist/temp/src/components/overlays/notification/__tests__/Notification.test.d.ts +1 -0
- package/dist/temp/src/components/overlays/notification/__tests__/Notification.test.js +71 -0
- package/dist/temp/src/components/overlays/postcode-search-modal/PostcodeSearchModal.d.ts +2 -6
- package/dist/temp/src/components/overlays/postcode-search-modal/PostcodeSearchModal.js +38 -27
- package/dist/temp/src/components/overlays/postcode-search-modal/PostcodeSearchModalTip.d.ts +16 -0
- package/dist/temp/src/components/overlays/postcode-search-modal/PostcodeSearchModalTip.js +24 -0
- package/dist/temp/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.js +82 -35
- package/dist/temp/src/generated/scoped-css.js +1 -1
- package/dist/types/src/components/feedback-and-status/badge/Badge.d.ts +1 -1
- package/dist/types/src/components/overlays/notification/CalloutNotification.d.ts +3 -1
- package/dist/types/src/components/overlays/notification/Notification.d.ts +12 -2
- package/dist/types/src/components/overlays/notification/__tests__/CalloutNotification.test.d.ts +1 -0
- package/dist/types/src/components/overlays/notification/__tests__/Notification.test.d.ts +1 -0
- package/dist/types/src/components/overlays/postcode-search-modal/PostcodeSearchModal.d.ts +2 -6
- package/dist/types/src/components/overlays/postcode-search-modal/PostcodeSearchModalTip.d.ts +16 -0
- package/dist/ui-admin/assets/styles/style.css +91 -33
- package/package.json +1 -1
- package/dist/ui-admin/assets/styles/style.scoped.css +0 -8557
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { AnnotationInfoFill } from '@ncds/ui-admin-icon';
|
|
3
2
|
import classnames from 'classnames';
|
|
4
|
-
import { useEffect, useState } from 'react';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
4
|
import { Button } from '../../action/button';
|
|
6
5
|
import { EmptyState } from '../../feedback-and-status/empty-state';
|
|
7
6
|
import { InputBase } from '../../forms-and-input/input-base';
|
|
8
7
|
import { Radio } from '../../forms-and-input/radio';
|
|
9
8
|
import { Pagination } from '../../navigation/pagination';
|
|
10
9
|
import { Modal } from '../modal';
|
|
10
|
+
import { Tip } from './PostcodeSearchModalTip';
|
|
11
11
|
/** 기획서 기준값 */
|
|
12
12
|
const PAGE_SIZE = 15;
|
|
13
13
|
/** 페이지 번호 그룹 크기 — 초과 시 Pagination 이 점프 버튼을 노출한다 */
|
|
14
14
|
const PAGER_WINDOW = 10;
|
|
15
|
-
/**
|
|
16
|
-
const
|
|
15
|
+
/** 검색 조합 안내 문구를 띄우는 기준 — 결과가 이 수를 넘을 때만 노출한다 */
|
|
16
|
+
const SEARCH_HINT_MIN_TOTAL_COUNT = 30;
|
|
17
17
|
/** 반응형 전환 기준 — assets/styles/base/_variable.scss 의 $breakpoint 와 동일 */
|
|
18
18
|
const MOBILE_BREAKPOINT = 768;
|
|
19
|
+
/** 본문 스크롤 컨테이너. Modal.Content 가 overflow-y:auto 를 갖는다. */
|
|
20
|
+
const SCROLL_CONTAINER_SELECTOR = '.ncua-modal__content';
|
|
19
21
|
const MODAL_TITLE = '주소 찾기';
|
|
20
22
|
const SEARCH_PLACEHOLDER = '도로명, 지번, 건물명 검색';
|
|
21
23
|
const SEARCH_BUTTON_LABEL = '검색';
|
|
@@ -23,7 +25,6 @@ const EMPTY_TITLE = '검색 결과가 없습니다.';
|
|
|
23
25
|
const EMPTY_DESCRIPTION = '검색어를 다시 확인해 주세요.';
|
|
24
26
|
const ROAD_BADGE_LABEL = '도로명';
|
|
25
27
|
const JIBUN_BADGE_LABEL = '지번';
|
|
26
|
-
const TIP_LABEL = 'Tip';
|
|
27
28
|
// 라디오 그룹 이름. `ncua-` 로 시작하면 MCP 의 BEM 추출기가 CSS 클래스로 오인하므로 접두사를 뺀다.
|
|
28
29
|
const RESULT_RADIO_NAME = 'postcode-search-modal-result';
|
|
29
30
|
/** E0006·E0008 외 나머지 코드는 모두 같은 문구를 쓴다(기획서: E0009~E0013 통합). */
|
|
@@ -32,23 +33,6 @@ const ERROR_HINT = {
|
|
|
32
33
|
E0006: '시도명으로는 검색이 불가합니다.',
|
|
33
34
|
E0008: '검색어를 두 글자 이상 입력해주세요.'
|
|
34
35
|
};
|
|
35
|
-
/** Tip 카드 첫 줄 안내 문구 (기획서 TIPS.searchTitle) */
|
|
36
|
-
const TIP_TITLE = '아래 조합으로 검색하면 원하는 주소를 더 쉽게 찾을 수 있어요.';
|
|
37
|
-
/** Tip 상세 버전에 노출할 검색어 조합과 예시 주소 (기획서 TIPS.searchTips) */
|
|
38
|
-
const TIP_ITEMS = [{
|
|
39
|
-
combination: '도로명 + 건물번호',
|
|
40
|
-
example: '디지털로26길 43'
|
|
41
|
-
}, {
|
|
42
|
-
combination: '지역명(동·리) + 번지',
|
|
43
|
-
example: '구로동 235-3'
|
|
44
|
-
}, {
|
|
45
|
-
combination: '지역명(동·리) + 건물명(아파트명)',
|
|
46
|
-
example: '구로동 대륭포스트타워8차'
|
|
47
|
-
}];
|
|
48
|
-
/** Tip 압축 버전 문구 — 예시 주소를 빼고 조합만 한 줄로 (기획서 TIPS.searchTipsCompact) */
|
|
49
|
-
const TIP_COMPACT_TEXT = "'도로명+건물번호', '지역명+지번', '지역명+건물명(아파트명)'";
|
|
50
|
-
const TIP_EXAMPLE_PREFIX = '예)';
|
|
51
|
-
const TIP_ICON_SIZE = 16;
|
|
52
36
|
/** 선택 표시를 판정하는 키. 우편번호만으로는 같은 건물의 다른 지번을 구분하지 못한다. */
|
|
53
37
|
const resultKey = _ref => {
|
|
54
38
|
let {
|
|
@@ -58,10 +42,6 @@ const resultKey = _ref => {
|
|
|
58
42
|
} = _ref;
|
|
59
43
|
return `${zipCode}|${roadAddress}|${jibunAddress}`;
|
|
60
44
|
};
|
|
61
|
-
const resolveTipVariant = (hasResults, totalCount) => {
|
|
62
|
-
if (!hasResults) return 'detail';
|
|
63
|
-
return totalCount > TIP_COMPACT_THRESHOLD ? 'compact' : null;
|
|
64
|
-
};
|
|
65
45
|
/** SSR(도큐사우루스 등)에서는 초기값을 false(=PC)로 두고 mount 이후 갱신한다. */
|
|
66
46
|
const useIsMobileViewport = () => {
|
|
67
47
|
const [isMobile, setIsMobile] = useState(false);
|
|
@@ -77,49 +57,33 @@ const useIsMobileViewport = () => {
|
|
|
77
57
|
}, []);
|
|
78
58
|
return isMobile;
|
|
79
59
|
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
example
|
|
106
|
-
} = _ref3;
|
|
107
|
-
return _jsxs("li", {
|
|
108
|
-
className: "ncua-postcode-search-modal__tip-item",
|
|
109
|
-
children: [_jsx("span", {
|
|
110
|
-
className: "ncua-postcode-search-modal__tip-combination",
|
|
111
|
-
children: combination
|
|
112
|
-
}), _jsx("span", {
|
|
113
|
-
className: "ncua-postcode-search-modal__tip-example",
|
|
114
|
-
children: `${TIP_EXAMPLE_PREFIX} ${example}`
|
|
115
|
-
})]
|
|
116
|
-
}, combination);
|
|
117
|
-
})
|
|
118
|
-
})]
|
|
119
|
-
});
|
|
60
|
+
/**
|
|
61
|
+
* 본문이 위로 스크롤됐는지 여부.
|
|
62
|
+
* 고정된 검색 영역과 스크롤되는 결과 사이의 경계선을 스크롤 중에만 그리기 위해 쓴다.
|
|
63
|
+
* enabled 가 false 면 감시를 걸지 않고 항상 false 를 돌려준다.
|
|
64
|
+
*/
|
|
65
|
+
const useIsContentScrolled = (rootRef, enabled) => {
|
|
66
|
+
const [isScrolled, setIsScrolled] = useState(false);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!enabled) {
|
|
69
|
+
setIsScrolled(false);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const scroller = rootRef.current?.closest(SCROLL_CONTAINER_SELECTOR);
|
|
73
|
+
if (!scroller) return;
|
|
74
|
+
const sync = () => setIsScrolled(scroller.scrollTop > 0);
|
|
75
|
+
// 결과가 바뀌어도 스크롤 위치는 남을 수 있어 구독 시점의 값으로 먼저 맞춘다.
|
|
76
|
+
sync();
|
|
77
|
+
scroller.addEventListener('scroll', sync, {
|
|
78
|
+
passive: true
|
|
79
|
+
});
|
|
80
|
+
return () => {
|
|
81
|
+
scroller.removeEventListener('scroll', sync);
|
|
82
|
+
};
|
|
83
|
+
}, [rootRef, enabled]);
|
|
84
|
+
return isScrolled;
|
|
120
85
|
};
|
|
121
|
-
|
|
122
|
-
const PostcodeSearchModal = _ref4 => {
|
|
86
|
+
const PostcodeSearchModal = _ref2 => {
|
|
123
87
|
let {
|
|
124
88
|
open,
|
|
125
89
|
onClose,
|
|
@@ -136,8 +100,9 @@ const PostcodeSearchModal = _ref4 => {
|
|
|
136
100
|
loading = false,
|
|
137
101
|
errorCode,
|
|
138
102
|
onSelect
|
|
139
|
-
} =
|
|
103
|
+
} = _ref2;
|
|
140
104
|
const isMobile = useIsMobileViewport();
|
|
105
|
+
const rootRef = useRef(null);
|
|
141
106
|
// 선택 표시는 컴포넌트가 직접 들고 있는다. 사용처는 onSelect 로 값만 받으면 된다.
|
|
142
107
|
// 우편번호는 한 페이지에 중복될 수 있어 주소 전체를 키로 쓴다.
|
|
143
108
|
const [selectedKey, setSelectedKey] = useState();
|
|
@@ -154,10 +119,13 @@ const PostcodeSearchModal = _ref4 => {
|
|
|
154
119
|
}, [open]);
|
|
155
120
|
const hasError = Boolean(errorCode);
|
|
156
121
|
const hasResults = !hasError && results.length > 0;
|
|
122
|
+
// 결과가 적으면 굳이 검색 조합을 안내하지 않는다.
|
|
123
|
+
const showSearchHint = hasResults && totalCount > SEARCH_HINT_MIN_TOTAL_COUNT;
|
|
157
124
|
// 검색을 실행한 검색어와 현재 입력값이 같을 때만 "결과 없음"으로 본다.
|
|
158
125
|
// 입력 중에는 두 값이 달라지고, 응답을 기다리는 동안에는 loading 이 막는다.
|
|
159
126
|
const showEmptyState = !hasError && !loading && results.length === 0 && searchQuery !== '' && searchQuery === searchedQuery;
|
|
160
|
-
|
|
127
|
+
// 결과 목록이 있을 때만 스크롤이 생기므로 그때만 감시한다.
|
|
128
|
+
const isContentScrolled = useIsContentScrolled(rootRef, open && hasResults);
|
|
161
129
|
const handleSearchQueryChange = event => {
|
|
162
130
|
onSearchQueryChange(event.target.value);
|
|
163
131
|
};
|
|
@@ -184,28 +152,42 @@ const PostcodeSearchModal = _ref4 => {
|
|
|
184
152
|
}), _jsx(Modal.Content, {
|
|
185
153
|
children: _jsxs("div", {
|
|
186
154
|
className: "ncua-postcode-search-modal",
|
|
155
|
+
ref: rootRef,
|
|
187
156
|
children: [_jsxs("div", {
|
|
188
|
-
className:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
157
|
+
className: classnames('ncua-postcode-search-modal__search-area', {
|
|
158
|
+
'is-fixed': hasResults && isContentScrolled
|
|
159
|
+
}),
|
|
160
|
+
children: [_jsxs("div", {
|
|
161
|
+
className: "ncua-postcode-search-modal__search",
|
|
162
|
+
children: [_jsx(InputBase, {
|
|
163
|
+
className: "ncua-postcode-search-modal__search-input",
|
|
164
|
+
size: "sm",
|
|
165
|
+
fullWidth: true,
|
|
166
|
+
value: searchQuery,
|
|
167
|
+
clearText: !!searchQuery,
|
|
168
|
+
onClearText: () => onSearchQueryChange(''),
|
|
169
|
+
onChange: handleSearchQueryChange,
|
|
170
|
+
onKeyDown: handleSearchKeyDown,
|
|
171
|
+
placeholder: SEARCH_PLACEHOLDER,
|
|
172
|
+
destructive: hasError,
|
|
173
|
+
hintText: errorCode ? ERROR_HINT[errorCode] ?? DEFAULT_ERROR_HINT : undefined
|
|
174
|
+
}), _jsx(Button, {
|
|
175
|
+
className: "ncua-postcode-search-modal__search-button",
|
|
176
|
+
label: SEARCH_BUTTON_LABEL,
|
|
177
|
+
size: "sm",
|
|
178
|
+
hierarchy: "secondary",
|
|
179
|
+
onClick: handleSearch
|
|
180
|
+
})]
|
|
181
|
+
}), showSearchHint && _jsxs("div", {
|
|
182
|
+
children: [_jsx("p", {
|
|
183
|
+
className: "ncua-postcode-search-modal__result-title",
|
|
184
|
+
children: "\uC544\uB798 \uC870\uD569\uC73C\uB85C \uAC80\uC0C9 \uC2DC \uC8FC\uC18C\uB97C \uB354 \uC27D\uAC8C \uCC3E\uC744 \uC218 \uC788\uC5B4\uC694."
|
|
185
|
+
}), _jsx("p", {
|
|
186
|
+
className: "ncua-postcode-search-modal__result-desc",
|
|
187
|
+
children: "\uB3C4\uB85C\uBA85+\uAC74\uBB3C\uBC88\uD638, \uC9C0\uC5ED\uBA85+\uC9C0\uBC88, \uC9C0\uC5ED\uBA85+\uAC74\uBB3C\uBA85(\uC544\uD30C\uD2B8\uBA85)"
|
|
188
|
+
})]
|
|
205
189
|
})]
|
|
206
|
-
}),
|
|
207
|
-
compact: tipVariant === 'compact'
|
|
208
|
-
}), hasResults && _jsxs(_Fragment, {
|
|
190
|
+
}), !hasResults && _jsx(PostcodeSearchModal.Tip, {}), hasResults && _jsxs(_Fragment, {
|
|
209
191
|
children: [_jsxs("p", {
|
|
210
192
|
className: "ncua-postcode-search-modal__count",
|
|
211
193
|
children: ["\uAC80\uC0C9 ", _jsx("b", {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AnnotationInfoFill } from '@ncds/ui-admin-icon';
|
|
3
|
+
import classnames from 'classnames';
|
|
4
|
+
const TIP_LABEL = 'Tip';
|
|
5
|
+
/** Tip 카드 첫 줄 안내 문구 (기획서 TIPS.searchTitle) */
|
|
6
|
+
const TIP_TITLE = '아래 조합으로 검색하면 원하는 주소를 더 쉽게 찾을 수 있어요.';
|
|
7
|
+
/** Tip 에 노출할 검색어 조합과 예시 주소 (기획서 TIPS.searchTips) */
|
|
8
|
+
const TIP_ITEMS = [{
|
|
9
|
+
combination: '도로명 + 건물번호',
|
|
10
|
+
example: '디지털로26길 43'
|
|
11
|
+
}, {
|
|
12
|
+
combination: '지역명(동·리) + 번지',
|
|
13
|
+
example: '구로동 235-3'
|
|
14
|
+
}, {
|
|
15
|
+
combination: '지역명(동·리) + 건물명(아파트명)',
|
|
16
|
+
example: '구로동 대륭포스트타워8차'
|
|
17
|
+
}];
|
|
18
|
+
const TIP_EXAMPLE_PREFIX = '예)';
|
|
19
|
+
const TIP_ICON_SIZE = 16;
|
|
20
|
+
/**
|
|
21
|
+
* 검색어 조합을 안내하는 Tip 카드.
|
|
22
|
+
* 결과 목록이 없는 화면(초기·오류·결과 없음)에서만 노출된다.
|
|
23
|
+
* 결과가 있을 때는 모달이 안내 문구 한 줄만 검색 영역에 붙여 고정한다.
|
|
24
|
+
*/
|
|
25
|
+
const Tip = _ref => {
|
|
26
|
+
let {
|
|
27
|
+
className
|
|
28
|
+
} = _ref;
|
|
29
|
+
return _jsxs("div", {
|
|
30
|
+
className: classnames('ncua-postcode-search-modal__tip', className),
|
|
31
|
+
children: [_jsxs("p", {
|
|
32
|
+
className: "ncua-postcode-search-modal__tip-label",
|
|
33
|
+
children: [_jsx(AnnotationInfoFill, {
|
|
34
|
+
className: "ncua-postcode-search-modal__tip-icon",
|
|
35
|
+
width: TIP_ICON_SIZE,
|
|
36
|
+
height: TIP_ICON_SIZE
|
|
37
|
+
}), TIP_LABEL]
|
|
38
|
+
}), _jsx("p", {
|
|
39
|
+
className: "ncua-postcode-search-modal__tip-title",
|
|
40
|
+
children: TIP_TITLE
|
|
41
|
+
}), _jsx("ul", {
|
|
42
|
+
className: "ncua-postcode-search-modal__tip-list",
|
|
43
|
+
children: TIP_ITEMS.map(_ref2 => {
|
|
44
|
+
let {
|
|
45
|
+
combination,
|
|
46
|
+
example
|
|
47
|
+
} = _ref2;
|
|
48
|
+
return _jsxs("li", {
|
|
49
|
+
className: "ncua-postcode-search-modal__tip-item",
|
|
50
|
+
children: [_jsx("span", {
|
|
51
|
+
className: "ncua-postcode-search-modal__tip-combination",
|
|
52
|
+
children: combination
|
|
53
|
+
}), _jsx("span", {
|
|
54
|
+
className: "ncua-postcode-search-modal__tip-example",
|
|
55
|
+
children: `${TIP_EXAMPLE_PREFIX} ${example}`
|
|
56
|
+
})]
|
|
57
|
+
}, combination);
|
|
58
|
+
})
|
|
59
|
+
})]
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
Tip.displayName = 'PostcodeSearchModal.Tip';
|
|
63
|
+
export { Tip, TIP_TITLE };
|
package/dist/esm/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.js
CHANGED
|
@@ -7,11 +7,11 @@ import { PostcodeSearchModal } from '../PostcodeSearchModal';
|
|
|
7
7
|
// React 18에서 act() 사용 시 필요한 플래그 (미설정 시 console.error 경고 발생)
|
|
8
8
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
9
|
const PAGE_SIZE = 15;
|
|
10
|
-
/**
|
|
10
|
+
/** 한 페이지에 다 들어가는 결과 건수 — Tip 카드 미노출 케이스 */
|
|
11
11
|
const SMALL_RESULT_COUNT = 5;
|
|
12
12
|
/** SMALL_RESULT_COUNT 기준 총 페이지 수 */
|
|
13
13
|
const SMALL_RESULT_TOTAL_PAGE = 1;
|
|
14
|
-
/**
|
|
14
|
+
/** 여러 페이지로 나뉘는 결과 건수 — totalPage(3) <= 페이지 그룹 크기(10) */
|
|
15
15
|
const LARGE_RESULT_COUNT = 45;
|
|
16
16
|
/** LARGE_RESULT_COUNT 기준 총 페이지 수 */
|
|
17
17
|
const LARGE_RESULT_TOTAL_PAGE = 3;
|
|
@@ -19,6 +19,10 @@ const LARGE_RESULT_TOTAL_PAGE = 3;
|
|
|
19
19
|
const HUGE_RESULT_COUNT = 200;
|
|
20
20
|
/** 클릭 대상 페이지 번호 */
|
|
21
21
|
const TARGET_PAGE = 3;
|
|
22
|
+
/** 본문을 아래로 내린 상태를 흉내 낼 scrollTop 값 */
|
|
23
|
+
const SCROLLED_OFFSET = 120;
|
|
24
|
+
/** 맨 위로 되돌린 scrollTop 값 */
|
|
25
|
+
const SCROLL_TOP = 0;
|
|
22
26
|
/** 결과 더미 생성 기준값 */
|
|
23
27
|
const ZIP_CODE_BASE = 13529;
|
|
24
28
|
const ROAD_NUMBER_BASE = 242;
|
|
@@ -93,7 +97,23 @@ function baseProps() {
|
|
|
93
97
|
};
|
|
94
98
|
}
|
|
95
99
|
const tipOf = el => el.querySelector('.ncua-postcode-search-modal__tip');
|
|
96
|
-
const
|
|
100
|
+
const searchAreaOf = el => el.querySelector('.ncua-postcode-search-modal__search-area');
|
|
101
|
+
const isFixedSearchArea = el => Boolean(searchAreaOf(el)?.classList.contains('is-fixed'));
|
|
102
|
+
const scrollerOf = el => el.querySelector('.ncua-modal__content');
|
|
103
|
+
/** jsdom 은 레이아웃이 없어 scrollTop 이 항상 0 이므로, 값을 덮어쓰고 scroll 이벤트를 쏜다. */
|
|
104
|
+
function scrollContent(el, scrollTop) {
|
|
105
|
+
const scroller = scrollerOf(el);
|
|
106
|
+
if (!scroller) throw new Error('모달 본문 스크롤 컨테이너를 찾을 수 없습니다');
|
|
107
|
+
Object.defineProperty(scroller, 'scrollTop', {
|
|
108
|
+
value: scrollTop,
|
|
109
|
+
configurable: true
|
|
110
|
+
});
|
|
111
|
+
act(() => {
|
|
112
|
+
scroller.dispatchEvent(new Event('scroll'));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const resultTitleOf = el => el.querySelector('.ncua-postcode-search-modal__result-title');
|
|
116
|
+
const resultDescOf = el => el.querySelector('.ncua-postcode-search-modal__result-desc');
|
|
97
117
|
const countLabelOf = el => el.querySelector('.ncua-postcode-search-modal__count');
|
|
98
118
|
const itemsOf = el => el.querySelectorAll('.ncua-postcode-search-modal__item');
|
|
99
119
|
const inputRootOf = el => el.querySelector('.ncua-input');
|
|
@@ -113,13 +133,13 @@ afterEach(() => {
|
|
|
113
133
|
vi.unstubAllGlobals();
|
|
114
134
|
});
|
|
115
135
|
describe('PostcodeSearchModal — Case 1: 검색 전 초기 상태', () => {
|
|
116
|
-
it('
|
|
136
|
+
it('Tip 카드만 노출되고 카운트·리스트·페이지네이션·EmptyState 는 렌더되지 않는다', () => {
|
|
117
137
|
// Given: 검색어·결과가 비어 있고 에러도 없는 상태에서
|
|
118
138
|
// When: 모달을 열면
|
|
119
139
|
const el = render(baseProps());
|
|
120
|
-
// Then:
|
|
140
|
+
// Then: Tip 카드만 노출되고 결과 영역은 전부 미노출이다
|
|
121
141
|
expect(tipOf(el)).not.toBeNull();
|
|
122
|
-
expect(
|
|
142
|
+
expect(resultTitleOf(el)).toBeNull();
|
|
123
143
|
expect(countLabelOf(el)).toBeNull();
|
|
124
144
|
expect(el.querySelector('.ncua-postcode-search-modal__list')).toBeNull();
|
|
125
145
|
expect(paginationOf(el)).toBeNull();
|
|
@@ -154,15 +174,15 @@ describe('PostcodeSearchModal — Case 2~4: 에러코드별 hint 매핑', () =>
|
|
|
154
174
|
expect(inputRootOf(el)?.classList.contains('destructive')).toBe(true);
|
|
155
175
|
expect(hintOf(el)?.textContent).toBe(hint);
|
|
156
176
|
});
|
|
157
|
-
it('에러 상태에서는
|
|
177
|
+
it('에러 상태에서는 Tip 카드가 노출되고 리스트·EmptyState 는 미노출이다', () => {
|
|
158
178
|
// Given/When: 에러코드를 전달해 렌더하면
|
|
159
179
|
const el = render(baseProps({
|
|
160
180
|
searchQuery: '서',
|
|
161
181
|
errorCode: 'E0008'
|
|
162
182
|
}));
|
|
163
|
-
// Then:
|
|
183
|
+
// Then: Tip 카드만 남는다
|
|
164
184
|
expect(tipOf(el)).not.toBeNull();
|
|
165
|
-
expect(
|
|
185
|
+
expect(resultTitleOf(el)).toBeNull();
|
|
166
186
|
expect(el.querySelector('.ncua-postcode-search-modal__list')).toBeNull();
|
|
167
187
|
expect(emptyStateOf(el)).toBeNull();
|
|
168
188
|
expect(paginationOf(el)).toBeNull();
|
|
@@ -182,11 +202,11 @@ describe('PostcodeSearchModal — Case 5: 검색 결과 없음', () => {
|
|
|
182
202
|
results: [],
|
|
183
203
|
totalCount: 0
|
|
184
204
|
}));
|
|
185
|
-
// Then: "검색 결과가 없습니다." 는 뜨지 않고
|
|
205
|
+
// Then: "검색 결과가 없습니다." 는 뜨지 않고 Tip 카드만 유지된다
|
|
186
206
|
expect(emptyStateOf(el)).toBeNull();
|
|
187
207
|
expect(tipOf(el)).not.toBeNull();
|
|
188
208
|
});
|
|
189
|
-
it('EmptyState 가 "검색 결과가 없습니다." 로 렌더되고
|
|
209
|
+
it('EmptyState 가 "검색 결과가 없습니다." 로 렌더되고 Tip 카드가 유지된다', () => {
|
|
190
210
|
// Given: 검색어는 있으나 결과가 0건인 상태에서
|
|
191
211
|
const el = render(baseProps({
|
|
192
212
|
searchQuery: '존재하지않는주소',
|
|
@@ -200,14 +220,14 @@ describe('PostcodeSearchModal — Case 5: 검색 결과 없음', () => {
|
|
|
200
220
|
// Then: EmptyState 가 노출되고 오류 스타일은 아니며 페이지네이션은 없다
|
|
201
221
|
expect(emptyStateOf(el)?.textContent).toContain('검색 결과가 없습니다.');
|
|
202
222
|
expect(tipOf(el)).not.toBeNull();
|
|
203
|
-
expect(
|
|
223
|
+
expect(resultTitleOf(el)).toBeNull();
|
|
204
224
|
expect(inputRootOf(el)?.classList.contains('destructive')).toBe(false);
|
|
205
225
|
expect(paginationOf(el)).toBeNull();
|
|
206
226
|
expect(countLabelOf(el)).toBeNull();
|
|
207
227
|
});
|
|
208
228
|
});
|
|
209
|
-
describe('PostcodeSearchModal — Case 6: 결과
|
|
210
|
-
it('카운트 "검색 5개" 와 5행 리스트가 렌더되고 Tip
|
|
229
|
+
describe('PostcodeSearchModal — Case 6: 결과 있음', () => {
|
|
230
|
+
it('카운트 "검색 5개" 와 5행 리스트가 렌더되고 Tip 카드는 미노출된다', () => {
|
|
211
231
|
// Given/When: 5건 결과를 전달하면
|
|
212
232
|
const results = makeResults(SMALL_RESULT_COUNT);
|
|
213
233
|
const el = render(baseProps({
|
|
@@ -215,7 +235,7 @@ describe('PostcodeSearchModal — Case 6: 결과 30건 이하', () => {
|
|
|
215
235
|
results,
|
|
216
236
|
totalCount: SMALL_RESULT_COUNT
|
|
217
237
|
}));
|
|
218
|
-
// Then: 카운트·리스트가 노출되고 Tip
|
|
238
|
+
// Then: 카운트·리스트가 노출되고 Tip 카드는 사라진다
|
|
219
239
|
expect(countLabelOf(el)?.textContent).toBe(`검색 ${SMALL_RESULT_COUNT}개`);
|
|
220
240
|
expect(itemsOf(el)).toHaveLength(SMALL_RESULT_COUNT);
|
|
221
241
|
expect(tipOf(el)).toBeNull();
|
|
@@ -252,19 +272,34 @@ describe('PostcodeSearchModal — Case 6: 결과 30건 이하', () => {
|
|
|
252
272
|
expect(el.querySelectorAll('.ncua-pagination__page-num')).toHaveLength(SMALL_RESULT_TOTAL_PAGE);
|
|
253
273
|
});
|
|
254
274
|
});
|
|
255
|
-
describe('PostcodeSearchModal — Case 7: 결과
|
|
256
|
-
it('카운트 "검색 45개" 와 15행
|
|
275
|
+
describe('PostcodeSearchModal — Case 7: 결과 여러 페이지', () => {
|
|
276
|
+
it('카운트 "검색 45개" 와 15행 리스트가 렌더되고 Tip 카드 대신 결과 안내 문구가 남는다', () => {
|
|
257
277
|
// Given/When: 45건 중 첫 페이지 15건을 전달하면
|
|
258
278
|
const el = render(baseProps({
|
|
259
279
|
searchQuery: '판교로',
|
|
260
280
|
results: makeResults(PAGE_SIZE),
|
|
261
281
|
totalCount: LARGE_RESULT_COUNT
|
|
262
282
|
}));
|
|
263
|
-
// Then:
|
|
283
|
+
// Then: Tip 카드는 사라지고 검색 영역의 안내 문구(제목·조합)가 남는다
|
|
264
284
|
expect(countLabelOf(el)?.textContent).toBe(`검색 ${LARGE_RESULT_COUNT}개`);
|
|
265
285
|
expect(itemsOf(el)).toHaveLength(PAGE_SIZE);
|
|
266
|
-
expect(tipOf(el)).
|
|
267
|
-
expect(
|
|
286
|
+
expect(tipOf(el)).toBeNull();
|
|
287
|
+
expect(resultTitleOf(el)?.textContent).toBe('아래 조합으로 검색 시 주소를 더 쉽게 찾을 수 있어요.');
|
|
288
|
+
expect(resultDescOf(el)?.textContent).toBe('도로명+건물번호, 지역명+지번, 지역명+건물명(아파트명)');
|
|
289
|
+
});
|
|
290
|
+
it('검색 인풋과 안내 문구가 같은 고정 영역 안에 묶이고 결과 목록은 그 밖에 있다', () => {
|
|
291
|
+
// Given/When: 결과가 여러 페이지인 상태로 렌더하면
|
|
292
|
+
const el = render(baseProps({
|
|
293
|
+
searchQuery: '판교로',
|
|
294
|
+
results: makeResults(PAGE_SIZE),
|
|
295
|
+
totalCount: LARGE_RESULT_COUNT
|
|
296
|
+
}));
|
|
297
|
+
// Then: 상단 고정 영역이 인풋 행과 안내 문구를 함께 감싸고, 스크롤되는 목록은 밖에 남는다
|
|
298
|
+
const searchArea = searchAreaOf(el);
|
|
299
|
+
expect(searchArea?.querySelector('.ncua-postcode-search-modal__search')).not.toBeNull();
|
|
300
|
+
expect(searchArea?.querySelector('.ncua-postcode-search-modal__result-title')).not.toBeNull();
|
|
301
|
+
expect(searchArea?.querySelector('.ncua-postcode-search-modal__result-desc')).not.toBeNull();
|
|
302
|
+
expect(searchArea?.querySelector('.ncua-postcode-search-modal__list')).toBeNull();
|
|
268
303
|
});
|
|
269
304
|
it('totalPage(3) 가 PAGER_WINDOW(10) 이하이므로 점프 버튼은 노출되지 않는다', () => {
|
|
270
305
|
// Given/When: totalCount=45, PAGE_SIZE=15 → totalPage=3
|
|
@@ -291,7 +326,8 @@ describe('PostcodeSearchModal — Case 8: 결과 대량', () => {
|
|
|
291
326
|
expect(el.querySelector('.ncua-pagination__before')).not.toBeNull();
|
|
292
327
|
expect(el.querySelector('.ncua-pagination__next')).not.toBeNull();
|
|
293
328
|
expect(el.querySelector('.ncua-pagination__last')).not.toBeNull();
|
|
294
|
-
expect(
|
|
329
|
+
expect(tipOf(el)).toBeNull();
|
|
330
|
+
expect(resultTitleOf(el)).not.toBeNull();
|
|
295
331
|
});
|
|
296
332
|
it('3페이지 버튼을 클릭하면 onPageChange(3) 이 호출된다', () => {
|
|
297
333
|
// Given: 대량 결과가 렌더된 상태에서
|
|
@@ -521,8 +557,8 @@ describe('PostcodeSearchModal — 반응형 전환', () => {
|
|
|
521
557
|
});
|
|
522
558
|
});
|
|
523
559
|
describe('PostcodeSearchModal.Tip — 서브컴포넌트', () => {
|
|
524
|
-
it('
|
|
525
|
-
// Given/When:
|
|
560
|
+
it('조합 3종과 예시 주소를 모두 보여준다', () => {
|
|
561
|
+
// Given/When: Tip 카드가 노출되는 초기 상태로 렌더하면
|
|
526
562
|
const el = render(baseProps());
|
|
527
563
|
// Then: 안내 문구와 3개 조합, 예시 주소가 모두 포함된다
|
|
528
564
|
const tipText = tipOf(el)?.textContent ?? '';
|
|
@@ -534,23 +570,8 @@ describe('PostcodeSearchModal.Tip — 서브컴포넌트', () => {
|
|
|
534
570
|
expect(tipText).toContain('지역명(동·리) + 건물명(아파트명)');
|
|
535
571
|
expect(tipText).toContain('예) 구로동 대륭포스트타워8차');
|
|
536
572
|
});
|
|
537
|
-
it('압축 모드는 조합 3종만 보여주고 예시는 생략한다', () => {
|
|
538
|
-
// Given/When: 30건 초과 결과로 압축 Tip 을 렌더하면
|
|
539
|
-
const el = render(baseProps({
|
|
540
|
-
searchQuery: '판교로',
|
|
541
|
-
results: makeResults(PAGE_SIZE),
|
|
542
|
-
totalCount: LARGE_RESULT_COUNT
|
|
543
|
-
}));
|
|
544
|
-
// Then: 조합만 한 줄로 남고 예시 주소는 없다
|
|
545
|
-
const tipText = tipOf(el)?.textContent ?? '';
|
|
546
|
-
expect(tipText).toContain('아래 조합으로 검색하면 원하는 주소를 더 쉽게 찾을 수 있어요.');
|
|
547
|
-
expect(tipText).toContain("'도로명+건물번호', '지역명+지번', '지역명+건물명(아파트명)'");
|
|
548
|
-
expect(tipText).not.toContain('디지털로26길 43');
|
|
549
|
-
expect(tipText).not.toContain('구로동 235-3');
|
|
550
|
-
expect(tipText).not.toContain('구로동 대륭포스트타워8차');
|
|
551
|
-
});
|
|
552
573
|
it('Tip 아이콘은 하드코딩 SVG 가 아니라 아이콘 슬롯 클래스로 렌더된다', () => {
|
|
553
|
-
// Given/When:
|
|
574
|
+
// Given/When: Tip 카드를 렌더하면
|
|
554
575
|
const el = render(baseProps());
|
|
555
576
|
// Then: 지정한 아이콘 클래스를 가진 svg 가 존재한다
|
|
556
577
|
expect(tipOf(el)?.querySelector('svg.ncua-postcode-search-modal__tip-icon')).not.toBeNull();
|
|
@@ -560,4 +581,40 @@ describe('PostcodeSearchModal.Tip — 서브컴포넌트', () => {
|
|
|
560
581
|
expect(typeof PostcodeSearchModal.Tip).toBe('function');
|
|
561
582
|
expect(PostcodeSearchModal.Tip.displayName).toBe('PostcodeSearchModal.Tip');
|
|
562
583
|
});
|
|
584
|
+
});
|
|
585
|
+
describe('PostcodeSearchModal — 검색 영역 고정 표시', () => {
|
|
586
|
+
it('결과가 있어도 스크롤하기 전에는 is-fixed 가 붙지 않는다', () => {
|
|
587
|
+
// Given/When: 결과를 스크롤하지 않은 상태로 렌더하면
|
|
588
|
+
const el = render(baseProps({
|
|
589
|
+
searchQuery: '판교로',
|
|
590
|
+
results: makeResults(PAGE_SIZE),
|
|
591
|
+
totalCount: LARGE_RESULT_COUNT
|
|
592
|
+
}));
|
|
593
|
+
// Then: 검색 영역이 아직 떠 있지 않으므로 경계선 클래스도 없다
|
|
594
|
+
expect(isFixedSearchArea(el)).toBe(false);
|
|
595
|
+
});
|
|
596
|
+
it('결과를 스크롤하면 is-fixed 가 붙고, 맨 위로 돌아오면 다시 사라진다', () => {
|
|
597
|
+
// Given: 결과가 렌더된 상태에서
|
|
598
|
+
const el = render(baseProps({
|
|
599
|
+
searchQuery: '판교로',
|
|
600
|
+
results: makeResults(PAGE_SIZE),
|
|
601
|
+
totalCount: LARGE_RESULT_COUNT
|
|
602
|
+
}));
|
|
603
|
+
// When: 본문을 아래로 스크롤하면
|
|
604
|
+
scrollContent(el, SCROLLED_OFFSET);
|
|
605
|
+
// Then: 경계선 클래스가 붙는다
|
|
606
|
+
expect(isFixedSearchArea(el)).toBe(true);
|
|
607
|
+
// When: 다시 맨 위로 올리면
|
|
608
|
+
scrollContent(el, SCROLL_TOP);
|
|
609
|
+
// Then: 경계선 클래스가 사라진다
|
|
610
|
+
expect(isFixedSearchArea(el)).toBe(false);
|
|
611
|
+
});
|
|
612
|
+
it('결과가 없으면 스크롤이 일어나도 is-fixed 가 붙지 않는다', () => {
|
|
613
|
+
// Given: 초기 상태(결과 없음)로 렌더하고
|
|
614
|
+
const el = render(baseProps());
|
|
615
|
+
// When: 본문 스크롤 이벤트가 들어와도
|
|
616
|
+
scrollContent(el, SCROLLED_OFFSET);
|
|
617
|
+
// Then: 고정할 대상이 없으므로 클래스는 붙지 않는다
|
|
618
|
+
expect(isFixedSearchArea(el)).toBe(false);
|
|
619
|
+
});
|
|
563
620
|
});
|