@ncds/ui-admin 1.8.21-alpha.5 → 1.8.21-alpha.7

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 (71) hide show
  1. package/dist/cjs/assets/scripts/notification/FullWidthNotification.js +17 -6
  2. package/dist/cjs/assets/scripts/notification/MessageNotification.js +4 -2
  3. package/dist/cjs/assets/scripts/notification/messageTemplate.js +37 -8
  4. package/dist/cjs/constant/notification.js +21 -0
  5. package/dist/cjs/src/components/forms-and-input/input-base/InputBase.js +20 -14
  6. package/dist/cjs/src/components/index.js +11 -0
  7. package/dist/cjs/src/components/overlays/notification/FullWidthNotification.js +3 -1
  8. package/dist/cjs/src/components/overlays/notification/MessageNotification.js +2 -0
  9. package/dist/cjs/src/components/overlays/notification/MessageNotificationParts.js +3 -1
  10. package/dist/cjs/src/components/overlays/notification/__tests__/hidePermanentlyLabel.test.js +209 -0
  11. package/dist/cjs/src/components/overlays/postcode-search-modal/PostcodeSearchModal.js +283 -0
  12. package/dist/cjs/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.js +566 -0
  13. package/dist/cjs/src/components/overlays/postcode-search-modal/index.js +16 -0
  14. package/dist/cjs/src/generated/scoped-css.js +1 -1
  15. package/dist/esm/assets/scripts/notification/FullWidthNotification.js +17 -6
  16. package/dist/esm/assets/scripts/notification/MessageNotification.js +4 -2
  17. package/dist/esm/assets/scripts/notification/messageTemplate.js +37 -8
  18. package/dist/esm/constant/notification.js +15 -0
  19. package/dist/esm/src/components/forms-and-input/input-base/InputBase.js +20 -14
  20. package/dist/esm/src/components/index.js +1 -0
  21. package/dist/esm/src/components/overlays/notification/FullWidthNotification.js +3 -1
  22. package/dist/esm/src/components/overlays/notification/MessageNotification.js +2 -0
  23. package/dist/esm/src/components/overlays/notification/MessageNotificationParts.js +3 -1
  24. package/dist/esm/src/components/overlays/notification/__tests__/hidePermanentlyLabel.test.js +206 -0
  25. package/dist/esm/src/components/overlays/postcode-search-modal/PostcodeSearchModal.js +276 -0
  26. package/dist/esm/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.js +563 -0
  27. package/dist/esm/src/components/overlays/postcode-search-modal/index.js +1 -0
  28. package/dist/esm/src/generated/scoped-css.js +1 -1
  29. package/dist/temp/assets/scripts/notification/FullWidthNotification.d.ts +6 -0
  30. package/dist/temp/assets/scripts/notification/FullWidthNotification.js +14 -5
  31. package/dist/temp/assets/scripts/notification/MessageNotification.js +2 -1
  32. package/dist/temp/assets/scripts/notification/const/types.d.ts +10 -0
  33. package/dist/temp/assets/scripts/notification/messageTemplate.d.ts +1 -0
  34. package/dist/temp/assets/scripts/notification/messageTemplate.js +18 -8
  35. package/dist/temp/constant/notification.d.ts +15 -0
  36. package/dist/temp/constant/notification.js +15 -0
  37. package/dist/temp/src/components/forms-and-input/input-base/InputBase.js +18 -10
  38. package/dist/temp/src/components/index.d.ts +1 -0
  39. package/dist/temp/src/components/index.js +1 -0
  40. package/dist/temp/src/components/overlays/notification/FullWidthNotification.d.ts +6 -0
  41. package/dist/temp/src/components/overlays/notification/FullWidthNotification.js +3 -2
  42. package/dist/temp/src/components/overlays/notification/MessageNotification.d.ts +6 -0
  43. package/dist/temp/src/components/overlays/notification/MessageNotification.js +2 -2
  44. package/dist/temp/src/components/overlays/notification/MessageNotificationParts.d.ts +2 -1
  45. package/dist/temp/src/components/overlays/notification/MessageNotificationParts.js +3 -2
  46. package/dist/temp/src/components/overlays/notification/Notification.d.ts +8 -0
  47. package/dist/temp/src/components/overlays/notification/__tests__/hidePermanentlyLabel.test.d.ts +1 -0
  48. package/dist/temp/src/components/overlays/notification/__tests__/hidePermanentlyLabel.test.js +160 -0
  49. package/dist/temp/src/components/overlays/postcode-search-modal/PostcodeSearchModal.d.ts +49 -0
  50. package/dist/temp/src/components/overlays/postcode-search-modal/PostcodeSearchModal.js +118 -0
  51. package/dist/temp/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.d.ts +1 -0
  52. package/dist/temp/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.js +456 -0
  53. package/dist/temp/src/components/overlays/postcode-search-modal/index.d.ts +1 -0
  54. package/dist/temp/src/components/overlays/postcode-search-modal/index.js +1 -0
  55. package/dist/temp/src/generated/scoped-css.js +1 -1
  56. package/dist/types/assets/scripts/notification/FullWidthNotification.d.ts +6 -0
  57. package/dist/types/assets/scripts/notification/const/types.d.ts +10 -0
  58. package/dist/types/assets/scripts/notification/messageTemplate.d.ts +1 -0
  59. package/dist/types/constant/notification.d.ts +15 -0
  60. package/dist/types/src/components/index.d.ts +1 -0
  61. package/dist/types/src/components/overlays/notification/FullWidthNotification.d.ts +6 -0
  62. package/dist/types/src/components/overlays/notification/MessageNotification.d.ts +6 -0
  63. package/dist/types/src/components/overlays/notification/MessageNotificationParts.d.ts +2 -1
  64. package/dist/types/src/components/overlays/notification/Notification.d.ts +8 -0
  65. package/dist/types/src/components/overlays/notification/__tests__/hidePermanentlyLabel.test.d.ts +1 -0
  66. package/dist/types/src/components/overlays/postcode-search-modal/PostcodeSearchModal.d.ts +49 -0
  67. package/dist/types/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.d.ts +1 -0
  68. package/dist/types/src/components/overlays/postcode-search-modal/index.d.ts +1 -0
  69. package/dist/ui-admin/assets/styles/style.css +188 -13
  70. package/dist/ui-admin/assets/styles/style.scoped.css +188 -13
  71. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { HIDE_PERMANENTLY_LABEL } from '../../../constant/notification';
1
2
  import { sanitizeHtml } from '../utils/sanitize';
2
3
  import { CLASS_NAMES, FULL_WIDTH_ICON_MAP, FULL_WIDTH_SIZES, SVG_ICONS } from './const';
3
4
  import { bindNotificationEvents, createWrapperElement, renderActions, renderSupportingText, setupAutoClose } from './utils';
@@ -24,7 +25,8 @@ export class FullWidthNotification {
24
25
  actions,
25
26
  onClose,
26
27
  supportTextLink,
27
- onHidePermanently
28
+ onHidePermanently,
29
+ hidePermanentlyLabel
28
30
  } = this.options;
29
31
  const wrapper = createWrapperElement(CLASS_NAMES.FULL_WIDTH.BASE, color, className);
30
32
  const iconHtml = FULL_WIDTH_ICON_MAP[color](FULL_WIDTH_SIZES.ICON);
@@ -35,7 +37,8 @@ export class FullWidthNotification {
35
37
  actions,
36
38
  onClose,
37
39
  supportTextLink,
38
- onHidePermanently
40
+ onHidePermanently,
41
+ hidePermanentlyLabel
39
42
  }));
40
43
  return wrapper;
41
44
  }
@@ -47,7 +50,8 @@ export class FullWidthNotification {
47
50
  actions,
48
51
  onClose,
49
52
  supportTextLink,
50
- onHidePermanently
53
+ onHidePermanently,
54
+ hidePermanentlyLabel
51
55
  } = params;
52
56
  return `
53
57
  <div class="${CLASS_NAMES.FULL_WIDTH.CONTAINER}">
@@ -61,18 +65,25 @@ export class FullWidthNotification {
61
65
  </div>
62
66
  <div class="${CLASS_NAMES.FULL_WIDTH.ACTIONS_CONTAINER}">
63
67
  ${actions && actions.length > 0 ? renderActions(actions, CLASS_NAMES.FULL_WIDTH.ACTIONS) : ''}
64
- ${this.renderHidePermanentlyButton(onHidePermanently)}
68
+ ${this.renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel)}
65
69
  ${this.renderCloseButton(onClose)}
66
70
  </div>
67
71
  </div>
68
72
  </div>
69
73
  `;
70
74
  }
71
- renderHidePermanentlyButton(onHidePermanently) {
75
+ /**
76
+ * 다시보지 않기 버튼.
77
+ *
78
+ * `label`은 이 함수의 반환값이 `wrapper.innerHTML`에 들어가기 전에 `sanitizeHtml`로
79
+ * 처리되므로 별도 이스케이프하지 않는다 — `renderSupportingText`와 같은 정책이다.
80
+ */
81
+ renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel) {
72
82
  if (!onHidePermanently) return '';
83
+ const label = hidePermanentlyLabel ?? HIDE_PERMANENTLY_LABEL.fullWidth;
73
84
  return `
74
85
  <button type="button" class="ncua-notification__action-button ncua-notification__action-button--text ncua-full-width-notification__link" data-hide-permanently="true">
75
- 다시보지 않기
86
+ ${label}
76
87
  </button>
77
88
  `;
78
89
  }
@@ -25,7 +25,8 @@ export class MessageNotification {
25
25
  className,
26
26
  actions,
27
27
  onClose,
28
- onHidePermanently
28
+ onHidePermanently,
29
+ hidePermanentlyLabel
29
30
  } = this.options;
30
31
  // message 타입은 neutral, error, warning, success 4가지 색상만 지원
31
32
  let actualColor = color;
@@ -54,7 +55,8 @@ export class MessageNotification {
54
55
  supportingText,
55
56
  actions,
56
57
  onClose,
57
- onHidePermanently
58
+ onHidePermanently,
59
+ hidePermanentlyLabel
58
60
  }));
59
61
  // FeaturedIcon을 content-wrapper에 추가
60
62
  if (featuredIconElement) {
@@ -1,3 +1,4 @@
1
+ import { HIDE_PERMANENTLY_LABEL } from '../../../constant/notification';
1
2
  import { CLASS_NAMES, MESSAGE_SIZES, SVG_ICONS } from './const';
2
3
  import { renderActions, renderSupportingText } from './utils';
3
4
  /**
@@ -16,7 +17,8 @@ export function buildMessageTemplate(params) {
16
17
  supportingText,
17
18
  actions,
18
19
  onClose,
19
- onHidePermanently
20
+ onHidePermanently,
21
+ hidePermanentlyLabel
20
22
  } = params;
21
23
  return `
22
24
  <div class="${CLASS_NAMES.MESSAGE.CONTAINER}">
@@ -27,7 +29,12 @@ export function buildMessageTemplate(params) {
27
29
  ${renderSupportingText(supportingText, CLASS_NAMES.MESSAGE.SUPPORTING_TEXT)}
28
30
  </div>
29
31
  </div>
30
- ${renderActionsContainer(actions, onClose, onHidePermanently)}
32
+ ${renderActionsContainer({
33
+ actions,
34
+ onClose,
35
+ onHidePermanently,
36
+ hidePermanentlyLabel
37
+ })}
31
38
  </div>
32
39
  </div>
33
40
  `;
@@ -37,27 +44,49 @@ export function buildMessageTemplate(params) {
37
44
  * 이 컨테이너의 gap(12px)이 두 그룹 사이 간격을 만든다.
38
45
  * 표시할 요소가 하나도 없으면 빈 컨테이너를 남기지 않는다.
39
46
  */
40
- function renderActionsContainer(actions, onClose, onHidePermanently) {
47
+ function renderActionsContainer(params) {
48
+ const {
49
+ actions,
50
+ onClose,
51
+ onHidePermanently,
52
+ hidePermanentlyLabel
53
+ } = params;
41
54
  const actionsHtml = renderActions(actions ?? [], CLASS_NAMES.MESSAGE.ACTIONS, ACTION_BUTTON_SIZE);
42
- const footerHtml = renderFooterContainer(onClose, onHidePermanently);
55
+ const footerHtml = renderFooterContainer({
56
+ onClose,
57
+ onHidePermanently,
58
+ hidePermanentlyLabel
59
+ });
43
60
  if (!actionsHtml && !footerHtml) return '';
44
61
  return `<div class="${CLASS_NAMES.MESSAGE.ACTIONS_CONTAINER}">${actionsHtml}${footerHtml}</div>`;
45
62
  }
46
63
  /** 다시 보지 않기 + 닫기 그룹. 전용 래퍼를 쓰는 이유는 `CLASS_NAMES.MESSAGE.FOOTER_CONTAINER` 참조. */
47
- function renderFooterContainer(onClose, onHidePermanently) {
64
+ function renderFooterContainer(params) {
65
+ const {
66
+ onClose,
67
+ onHidePermanently,
68
+ hidePermanentlyLabel
69
+ } = params;
48
70
  if (!onHidePermanently && !onClose) return '';
49
71
  return `
50
72
  <div class="${CLASS_NAMES.MESSAGE.FOOTER_CONTAINER}">
51
- ${renderHidePermanentlyButton(onHidePermanently)}
73
+ ${renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel)}
52
74
  ${renderCloseButton(onClose)}
53
75
  </div>
54
76
  `;
55
77
  }
56
- function renderHidePermanentlyButton(onHidePermanently) {
78
+ /**
79
+ * 다시 보지 않기 버튼.
80
+ *
81
+ * `label`은 이 함수의 반환값이 `wrapper.innerHTML`에 들어가기 전에 `sanitizeHtml`로
82
+ * 처리되므로 별도 이스케이프하지 않는다 — `renderSupportingText`와 같은 정책이다.
83
+ */
84
+ function renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel) {
57
85
  if (!onHidePermanently) return '';
86
+ const label = hidePermanentlyLabel ?? HIDE_PERMANENTLY_LABEL.message;
58
87
  return `
59
88
  <button type="button" class="${CLASS_NAMES.COMMON.ACTION_BUTTON} ${CLASS_NAMES.MESSAGE.HIDE_LINK}" data-hide-permanently="true">
60
- 다시 보지 않기
89
+ ${label}
61
90
  </button>
62
91
  `;
63
92
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 다시보지 않기 버튼의 타입별 기본 문구.
3
+ *
4
+ * `hidePermanentlyLabel`을 지정하지 않았을 때 쓰인다.
5
+ * React(`src/components/overlays/notification/`)와 Vanilla(`assets/scripts/notification/`)
6
+ * 두 구현이 이 상수를 함께 참조한다 — 문구가 하드코딩으로 흩어져 서로 어긋나던 문제(#460)를
7
+ * 막기 위한 단일 출처다.
8
+ *
9
+ * `message`와 `fullWidth`의 띄어쓰기가 다른 것은 오타가 아니라 현행 유지다.
10
+ * 표기 통일은 별도 결정 사항이며, 결정되면 이 파일 한 곳만 고치면 된다.
11
+ */
12
+ export const HIDE_PERMANENTLY_LABEL = {
13
+ message: '다시 보지 않기',
14
+ fullWidth: '다시보지 않기'
15
+ };
@@ -32,26 +32,27 @@ const InputBase = /*#__PURE__*/forwardRef((_ref, ref) => {
32
32
  ...props
33
33
  } = _ref;
34
34
  const inputRef = useRef(null);
35
- const [textCount, setTextCount] = useState(0);
36
- const [hasValue, setHasValue] = useState(false);
37
- // 유무는 DOM에서 읽는다 — props.value 판정하면 uncontrolled Input에서 항상 빈 값으로 보인다.
38
- // biome-ignore lint/correctness/useExhaustiveDependencies: controlled 값이 프로그래밍적으로 바뀔 때는 input 이벤트가 나지 않아 props.value로 재동기화해야 한다
35
+ // controlled 인풋은 props.value 로 길이를 알 수 있으므로 상태를 두지 않는다.
36
+ // 입력마다 setState 하면 조합 중(한글 IME)에 리렌더가 끼어들어 조합이 끊긴다.
37
+ const isControlled = props.value !== undefined;
38
+ const [uncontrolledLength, setUncontrolledLength] = useState(0);
39
+ const valueLength = isControlled ? String(props.value ?? '').length : uncontrolledLength;
40
+ const hasValue = valueLength > 0;
41
+ const textCount = valueLength;
42
+ // uncontrolled 인풋만 DOM 값을 읽어 따라간다 — props.value 가 없어 길이를 알 방법이 이것뿐이다.
39
43
  useEffect(() => {
44
+ if (isControlled) return;
40
45
  const input = inputRef.current;
41
46
  if (!input) return;
42
47
  const syncFromInput = () => {
43
- const {
44
- length
45
- } = input.value;
46
- setHasValue(length > 0);
47
- setTextCount(length);
48
+ setUncontrolledLength(input.value.length);
48
49
  };
49
50
  syncFromInput();
50
51
  input.addEventListener('input', syncFromInput);
51
52
  return () => {
52
53
  input.removeEventListener('input', syncFromInput);
53
54
  };
54
- }, [props.value]);
55
+ }, [isControlled]);
55
56
  const renderOutsideSlot = (slot, position) => {
56
57
  if (slot.type === 'custom' && slot.placement !== 'outside') {
57
58
  return null;
@@ -65,10 +66,15 @@ const InputBase = /*#__PURE__*/forwardRef((_ref, ref) => {
65
66
  children: slot.label
66
67
  });
67
68
  case 'text':
68
- return _jsx("span", {
69
- className: classNames(`ncua-input__${position}-text`, `ncua-input__${position}-text--${size}`),
70
- children: slot.text
71
- });
69
+ {
70
+ // 클래스명은 리터럴로 쓴다 — MCP 의 BEM 추출기가 `ncua-input__${position}-text` 형태의
71
+ // 템플릿 리터럴에서는 `ncua-input__` 조각만 읽어 완성된 클래스를 잡지 못한다.
72
+ const textClass = position === 'leading' ? 'ncua-input__leading-text' : 'ncua-input__trailing-text';
73
+ return _jsx("span", {
74
+ className: classNames(textClass, `${textClass}--${size}`),
75
+ children: slot.text
76
+ });
77
+ }
72
78
  case 'custom':
73
79
  return slot.children;
74
80
  default:
@@ -51,6 +51,7 @@ export * from './navigation/vertical-tab';
51
51
  export * from './overlays/dropdown';
52
52
  export * from './overlays/modal';
53
53
  export * from './overlays/notification';
54
+ export * from './overlays/postcode-search-modal';
54
55
  export * from './overlays/tooltip';
55
56
  // Shared
56
57
  export * from './select-dropdown';
@@ -3,6 +3,7 @@ import { AlertTriangle, CheckCircle, InfoCircle, MessageChatSquare, XClose } fro
3
3
  import classNames from 'classnames';
4
4
  import { forwardRef, useEffect, useRef, useState } from 'react';
5
5
  import { COLOR } from '../../../../constant/color';
6
+ import { HIDE_PERMANENTLY_LABEL } from '../../../../constant/notification';
6
7
  const iconMap = {
7
8
  neutral: MessageChatSquare,
8
9
  error: AlertTriangle,
@@ -28,6 +29,7 @@ const FullWidthNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
28
29
  autoClose = 0,
29
30
  supportTextLink,
30
31
  onHidePermanently,
32
+ hidePermanentlyLabel = HIDE_PERMANENTLY_LABEL.fullWidth,
31
33
  ...rest
32
34
  } = _ref;
33
35
  const [shouldRemove, setShouldRemove] = useState(false);
@@ -109,7 +111,7 @@ const FullWidthNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
109
111
  type: "button",
110
112
  className: classNames('ncua-notification__action-button', 'ncua-notification__action-button--text', 'ncua-full-width-notification__link'),
111
113
  onClick: onHidePermanently,
112
- children: "\uB2E4\uC2DC\uBCF4\uC9C0 \uC54A\uAE30"
114
+ children: hidePermanentlyLabel
113
115
  }), onClose && _jsx("button", {
114
116
  type: "button",
115
117
  className: "ncua-full-width-notification__close-button",
@@ -26,6 +26,7 @@ const MessageNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
26
26
  className,
27
27
  actions,
28
28
  onHidePermanently,
29
+ hidePermanentlyLabel,
29
30
  ...rest
30
31
  } = _ref;
31
32
  // message 타입은 neutral, error, warning, success 4가지 색상만 지원
@@ -49,6 +50,7 @@ const MessageNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
49
50
  actions: actions,
50
51
  onClose: onClose,
51
52
  onHidePermanently: onHidePermanently,
53
+ hidePermanentlyLabel: hidePermanentlyLabel,
52
54
  closeIconColor: COLOR[iconColorMap[validColor] || 'gray700']
53
55
  })]
54
56
  })
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Pin02, XClose } from '@ncds/ui-admin-icon';
3
+ import { HIDE_PERMANENTLY_LABEL } from '../../../../constant/notification';
3
4
  import { Button } from '../../action/button';
4
5
  import { FeaturedIcon } from '../../image-and-icons/featured-icon/FeaturedIcon';
5
6
  /** 아이콘 + 제목/서포팅 텍스트 영역. */
@@ -44,6 +45,7 @@ const MessageNotificationActions = _ref2 => {
44
45
  actions,
45
46
  onClose,
46
47
  onHidePermanently,
48
+ hidePermanentlyLabel = HIDE_PERMANENTLY_LABEL.message,
47
49
  closeIconColor
48
50
  } = _ref2;
49
51
  const hasActions = Boolean(actions?.length);
@@ -65,7 +67,7 @@ const MessageNotificationActions = _ref2 => {
65
67
  type: "button",
66
68
  className: "ncua-notification__action-button ncua-message-notification__hide-link",
67
69
  onClick: onHidePermanently,
68
- children: "\uB2E4\uC2DC \uBCF4\uC9C0 \uC54A\uAE30"
70
+ children: hidePermanentlyLabel
69
71
  }), onClose && _jsx("button", {
70
72
  type: "button",
71
73
  className: "ncua-message-notification__close-button",
@@ -0,0 +1,206 @@
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, vi } from 'vitest';
6
+ import { FullWidthNotification } from '../FullWidthNotification';
7
+ import { MessageNotification } from '../MessageNotification';
8
+ import { Notification } from '../Notification';
9
+ // React 18에서 act() 사용 시 필요한 플래그 (미설정 시 console.error 경고 발생)
10
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
11
+ const SELECTOR = {
12
+ /** message 타입 — 고도몰이 textContent 덮어쓰기에 쓰는 하위호환 계약 선택자. */
13
+ MESSAGE_HIDE_LINK: '.ncua-message-notification__hide-link',
14
+ /** full-width 타입 — 다시보지 않기 버튼. */
15
+ FULL_WIDTH_LINK: '.ncua-full-width-notification__link',
16
+ ACTION_BUTTON: '.ncua-notification__action-button'
17
+ };
18
+ /** 각 타입의 현행 기본 문구. 표기 통일은 별도 결정 사항이므로 서로 다른 값을 그대로 유지한다. */
19
+ const DEFAULT_LABEL = {
20
+ MESSAGE: '다시 보지 않기',
21
+ FULL_WIDTH: '다시보지 않기'
22
+ };
23
+ const mounted = [];
24
+ function render(element) {
25
+ const container = document.createElement('div');
26
+ document.body.appendChild(container);
27
+ const root = createRoot(container);
28
+ act(() => {
29
+ root.render(element);
30
+ });
31
+ mounted.push({
32
+ root,
33
+ container
34
+ });
35
+ return container;
36
+ }
37
+ function renderMessage() {
38
+ let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
39
+ return render(/*#__PURE__*/createElement(MessageNotification, {
40
+ title: '알림 제목',
41
+ ...props
42
+ }));
43
+ }
44
+ function renderFullWidth() {
45
+ let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
46
+ return render(/*#__PURE__*/createElement(FullWidthNotification, {
47
+ title: '알림 제목',
48
+ ...props
49
+ }));
50
+ }
51
+ function renderNotification() {
52
+ let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
53
+ return render(/*#__PURE__*/createElement(Notification, {
54
+ title: '알림 제목',
55
+ ...props
56
+ }));
57
+ }
58
+ afterEach(() => {
59
+ for (const {
60
+ root,
61
+ container
62
+ } of mounted.splice(0)) {
63
+ act(() => {
64
+ root.unmount();
65
+ });
66
+ container.remove();
67
+ }
68
+ });
69
+ describe('hidePermanentlyLabel (React) — message 타입', () => {
70
+ it('R-1: prop 미지정 시 기본 문구 "다시 보지 않기"가 렌더된다', () => {
71
+ // Given: hidePermanentlyLabel 없이 onHidePermanently만 주어지고
72
+ // When: MessageNotification을 렌더하면
73
+ const container = renderMessage({
74
+ onHidePermanently: vi.fn()
75
+ });
76
+ // Then: 현행 기본 문구가 그대로 노출된다 (시각적 변화 0)
77
+ const hideLink = container.querySelector(SELECTOR.MESSAGE_HIDE_LINK);
78
+ expect(hideLink?.textContent).toBe(DEFAULT_LABEL.MESSAGE);
79
+ });
80
+ it('R-2: prop 지정 시 그 값이 렌더된다', () => {
81
+ // Given: 커스텀 라벨이 주어지고
82
+ // When: 렌더하면
83
+ const container = renderMessage({
84
+ onHidePermanently: vi.fn(),
85
+ hidePermanentlyLabel: '30일간 보지 않기'
86
+ });
87
+ // Then: 하드코딩 문구 대신 전달한 값이 노출된다
88
+ const hideLink = container.querySelector(SELECTOR.MESSAGE_HIDE_LINK);
89
+ expect(hideLink?.textContent).toBe('30일간 보지 않기');
90
+ });
91
+ it('R-3: ReactNode 라벨을 받아 엘리먼트로 렌더한다', () => {
92
+ // Given: 문자열이 아닌 ReactNode 라벨이 주어지고
93
+ const label = /*#__PURE__*/createElement('span', {
94
+ className: 'custom-hide-label'
95
+ }, '커스텀 노드');
96
+ // When: 렌더하면
97
+ const container = renderMessage({
98
+ onHidePermanently: vi.fn(),
99
+ hidePermanentlyLabel: label
100
+ });
101
+ // Then: 노드가 hide-link 내부에 그대로 마운트된다
102
+ const hideLink = container.querySelector(SELECTOR.MESSAGE_HIDE_LINK);
103
+ expect(hideLink?.querySelector('.custom-hide-label')?.textContent).toBe('커스텀 노드');
104
+ });
105
+ it('R-4: 라벨을 바꿔도 __hide-link 클래스 계약이 유지된다', () => {
106
+ // Given: 커스텀 라벨이 주어지고
107
+ // When: 렌더하면
108
+ const container = renderMessage({
109
+ onHidePermanently: vi.fn(),
110
+ hidePermanentlyLabel: '커스텀'
111
+ });
112
+ // Then: 고도몰의 textContent 덮어쓰기가 계속 동작하도록 기존 클래스가 모두 남는다
113
+ const hideLink = container.querySelector(SELECTOR.MESSAGE_HIDE_LINK);
114
+ expect(hideLink).not.toBeNull();
115
+ expect(hideLink?.classList.contains('ncua-notification__action-button')).toBe(true);
116
+ expect(hideLink?.classList.contains('ncua-message-notification__hide-link')).toBe(true);
117
+ });
118
+ it('R-5: onHidePermanently 없이 라벨만 주면 버튼이 렌더되지 않는다', () => {
119
+ // Given: 핸들러 없이 라벨만 주어지고
120
+ // When: 렌더하면
121
+ const container = renderMessage({
122
+ hidePermanentlyLabel: '커스텀'
123
+ });
124
+ // Then: 라벨만으로는 버튼이 생기지 않는다 (핸들러가 렌더 조건)
125
+ expect(container.querySelector(SELECTOR.MESSAGE_HIDE_LINK)).toBeNull();
126
+ });
127
+ it('R-6: 라벨이 DOM 속성으로 새어나가지 않는다', () => {
128
+ // Given: 커스텀 라벨이 주어지고
129
+ // When: 렌더하면
130
+ const container = renderMessage({
131
+ onHidePermanently: vi.fn(),
132
+ hidePermanentlyLabel: '커스텀'
133
+ });
134
+ // Then: rest 스프레드를 타고 루트 div의 속성으로 흘러들지 않는다
135
+ const root = container.querySelector('.ncua-message-notification');
136
+ expect(root?.hasAttribute('hidepermanentlylabel')).toBe(false);
137
+ });
138
+ });
139
+ describe('hidePermanentlyLabel (React) — full-width 타입', () => {
140
+ it('R-7: prop 미지정 시 기본 문구 "다시보지 않기"가 렌더된다', () => {
141
+ // Given: hidePermanentlyLabel 없이 onHidePermanently만 주어지고
142
+ // When: FullWidthNotification을 렌더하면
143
+ const container = renderFullWidth({
144
+ onHidePermanently: vi.fn()
145
+ });
146
+ // Then: message 타입과 다른 현행 문구가 그대로 유지된다 (표기 통일은 별도 결정)
147
+ const hideButton = container.querySelector(SELECTOR.FULL_WIDTH_LINK);
148
+ expect(hideButton?.textContent).toBe(DEFAULT_LABEL.FULL_WIDTH);
149
+ });
150
+ it('R-8: prop 지정 시 그 값이 렌더된다', () => {
151
+ // Given: 커스텀 라벨이 주어지고
152
+ // When: 렌더하면
153
+ const container = renderFullWidth({
154
+ onHidePermanently: vi.fn(),
155
+ hidePermanentlyLabel: '오늘 하루 보지 않기'
156
+ });
157
+ // Then: 전달한 값이 노출된다
158
+ const hideButton = container.querySelector(SELECTOR.FULL_WIDTH_LINK);
159
+ expect(hideButton?.textContent).toBe('오늘 하루 보지 않기');
160
+ });
161
+ it('R-9: 라벨을 바꿔도 기존 클래스 계약이 유지된다', () => {
162
+ // Given: 커스텀 라벨이 주어지고
163
+ // When: 렌더하면
164
+ const container = renderFullWidth({
165
+ onHidePermanently: vi.fn(),
166
+ hidePermanentlyLabel: '커스텀'
167
+ });
168
+ // Then: 액션 버튼 클래스가 그대로 남는다
169
+ const hideButton = container.querySelector(SELECTOR.FULL_WIDTH_LINK);
170
+ expect(hideButton?.classList.contains('ncua-notification__action-button')).toBe(true);
171
+ expect(hideButton?.classList.contains('ncua-notification__action-button--text')).toBe(true);
172
+ });
173
+ it('R-10: onHidePermanently 없이 라벨만 주면 버튼이 렌더되지 않는다', () => {
174
+ // Given: 핸들러 없이 라벨만 주어지고
175
+ // When: 렌더하면
176
+ const container = renderFullWidth({
177
+ hidePermanentlyLabel: '커스텀'
178
+ });
179
+ // Then: 액션 버튼이 생기지 않는다
180
+ expect(container.querySelector(SELECTOR.ACTION_BUTTON)).toBeNull();
181
+ });
182
+ });
183
+ describe('hidePermanentlyLabel (React) — Notification 공유 Props 경유', () => {
184
+ it('R-11: type="message"에서 공유 Props의 라벨이 전달된다', () => {
185
+ // Given: 공유 Notification props로 라벨이 주어지고
186
+ // When: type="message"로 렌더하면
187
+ const container = renderNotification({
188
+ type: 'message',
189
+ onHidePermanently: vi.fn(),
190
+ hidePermanentlyLabel: '공유 프롭 라벨'
191
+ });
192
+ // Then: message 구현까지 라벨이 도달한다
193
+ expect(container.querySelector(SELECTOR.MESSAGE_HIDE_LINK)?.textContent).toBe('공유 프롭 라벨');
194
+ });
195
+ it('R-12: type="full-width"에서 공유 Props의 라벨이 전달된다', () => {
196
+ // Given: 공유 Notification props로 라벨이 주어지고
197
+ // When: type="full-width"로 렌더하면
198
+ const container = renderNotification({
199
+ type: 'full-width',
200
+ onHidePermanently: vi.fn(),
201
+ hidePermanentlyLabel: '공유 프롭 라벨'
202
+ });
203
+ // Then: full-width 구현까지 라벨이 도달한다
204
+ expect(container.querySelector(SELECTOR.FULL_WIDTH_LINK)?.textContent).toBe('공유 프롭 라벨');
205
+ });
206
+ });