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

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 (48) 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/overlays/notification/FullWidthNotification.js +3 -1
  6. package/dist/cjs/src/components/overlays/notification/MessageNotification.js +2 -0
  7. package/dist/cjs/src/components/overlays/notification/MessageNotificationParts.js +3 -1
  8. package/dist/cjs/src/components/overlays/notification/__tests__/hidePermanentlyLabel.test.js +209 -0
  9. package/dist/cjs/src/generated/scoped-css.js +1 -1
  10. package/dist/esm/assets/scripts/notification/FullWidthNotification.js +17 -6
  11. package/dist/esm/assets/scripts/notification/MessageNotification.js +4 -2
  12. package/dist/esm/assets/scripts/notification/messageTemplate.js +37 -8
  13. package/dist/esm/constant/notification.js +15 -0
  14. package/dist/esm/src/components/overlays/notification/FullWidthNotification.js +3 -1
  15. package/dist/esm/src/components/overlays/notification/MessageNotification.js +2 -0
  16. package/dist/esm/src/components/overlays/notification/MessageNotificationParts.js +3 -1
  17. package/dist/esm/src/components/overlays/notification/__tests__/hidePermanentlyLabel.test.js +206 -0
  18. package/dist/esm/src/generated/scoped-css.js +1 -1
  19. package/dist/temp/assets/scripts/notification/FullWidthNotification.d.ts +6 -0
  20. package/dist/temp/assets/scripts/notification/FullWidthNotification.js +14 -5
  21. package/dist/temp/assets/scripts/notification/MessageNotification.js +2 -1
  22. package/dist/temp/assets/scripts/notification/const/types.d.ts +10 -0
  23. package/dist/temp/assets/scripts/notification/messageTemplate.d.ts +1 -0
  24. package/dist/temp/assets/scripts/notification/messageTemplate.js +18 -8
  25. package/dist/temp/constant/notification.d.ts +15 -0
  26. package/dist/temp/constant/notification.js +15 -0
  27. package/dist/temp/src/components/overlays/notification/FullWidthNotification.d.ts +6 -0
  28. package/dist/temp/src/components/overlays/notification/FullWidthNotification.js +3 -2
  29. package/dist/temp/src/components/overlays/notification/MessageNotification.d.ts +6 -0
  30. package/dist/temp/src/components/overlays/notification/MessageNotification.js +2 -2
  31. package/dist/temp/src/components/overlays/notification/MessageNotificationParts.d.ts +2 -1
  32. package/dist/temp/src/components/overlays/notification/MessageNotificationParts.js +3 -2
  33. package/dist/temp/src/components/overlays/notification/Notification.d.ts +8 -0
  34. package/dist/temp/src/components/overlays/notification/__tests__/hidePermanentlyLabel.test.d.ts +1 -0
  35. package/dist/temp/src/components/overlays/notification/__tests__/hidePermanentlyLabel.test.js +160 -0
  36. package/dist/temp/src/generated/scoped-css.js +1 -1
  37. package/dist/types/assets/scripts/notification/FullWidthNotification.d.ts +6 -0
  38. package/dist/types/assets/scripts/notification/const/types.d.ts +10 -0
  39. package/dist/types/assets/scripts/notification/messageTemplate.d.ts +1 -0
  40. package/dist/types/constant/notification.d.ts +15 -0
  41. package/dist/types/src/components/overlays/notification/FullWidthNotification.d.ts +6 -0
  42. package/dist/types/src/components/overlays/notification/MessageNotification.d.ts +6 -0
  43. package/dist/types/src/components/overlays/notification/MessageNotificationParts.d.ts +2 -1
  44. package/dist/types/src/components/overlays/notification/Notification.d.ts +8 -0
  45. package/dist/types/src/components/overlays/notification/__tests__/hidePermanentlyLabel.test.d.ts +1 -0
  46. package/dist/ui-admin/assets/styles/style.css +2 -13
  47. package/dist/ui-admin/assets/styles/style.scoped.css +2 -13
  48. package/package.json +1 -1
@@ -9,6 +9,12 @@ export declare class FullWidthNotification {
9
9
  constructor(options: FullWidthNotificationOptions);
10
10
  private createElement;
11
11
  private buildTemplate;
12
+ /**
13
+ * 다시보지 않기 버튼.
14
+ *
15
+ * `label`은 이 함수의 반환값이 `wrapper.innerHTML`에 들어가기 전에 `sanitizeHtml`로
16
+ * 처리되므로 별도 이스케이프하지 않는다 — `renderSupportingText`와 같은 정책이다.
17
+ */
12
18
  private renderHidePermanentlyButton;
13
19
  private renderCloseButton;
14
20
  private bindEvents;
@@ -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';
@@ -16,7 +17,7 @@ export class FullWidthNotification {
16
17
  this.setupAutoClose();
17
18
  }
18
19
  createElement() {
19
- const { title, supportingText, color, className, actions, onClose, supportTextLink, onHidePermanently } = this.options;
20
+ const { title, supportingText, color, className, actions, onClose, supportTextLink, onHidePermanently, hidePermanentlyLabel, } = this.options;
20
21
  const wrapper = createWrapperElement(CLASS_NAMES.FULL_WIDTH.BASE, color, className);
21
22
  const iconHtml = FULL_WIDTH_ICON_MAP[color](FULL_WIDTH_SIZES.ICON);
22
23
  wrapper.innerHTML = sanitizeHtml(this.buildTemplate({
@@ -27,11 +28,12 @@ export class FullWidthNotification {
27
28
  onClose,
28
29
  supportTextLink,
29
30
  onHidePermanently,
31
+ hidePermanentlyLabel,
30
32
  }));
31
33
  return wrapper;
32
34
  }
33
35
  buildTemplate(params) {
34
- const { iconHtml, title, supportingText, actions, onClose, supportTextLink, onHidePermanently } = params;
36
+ const { iconHtml, title, supportingText, actions, onClose, supportTextLink, onHidePermanently, hidePermanentlyLabel, } = params;
35
37
  return `
36
38
  <div class="${CLASS_NAMES.FULL_WIDTH.CONTAINER}">
37
39
  <div class="${CLASS_NAMES.FULL_WIDTH.CONTENT}">
@@ -44,19 +46,26 @@ export class FullWidthNotification {
44
46
  </div>
45
47
  <div class="${CLASS_NAMES.FULL_WIDTH.ACTIONS_CONTAINER}">
46
48
  ${actions && actions.length > 0 ? renderActions(actions, CLASS_NAMES.FULL_WIDTH.ACTIONS) : ''}
47
- ${this.renderHidePermanentlyButton(onHidePermanently)}
49
+ ${this.renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel)}
48
50
  ${this.renderCloseButton(onClose)}
49
51
  </div>
50
52
  </div>
51
53
  </div>
52
54
  `;
53
55
  }
54
- renderHidePermanentlyButton(onHidePermanently) {
56
+ /**
57
+ * 다시보지 않기 버튼.
58
+ *
59
+ * `label`은 이 함수의 반환값이 `wrapper.innerHTML`에 들어가기 전에 `sanitizeHtml`로
60
+ * 처리되므로 별도 이스케이프하지 않는다 — `renderSupportingText`와 같은 정책이다.
61
+ */
62
+ renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel) {
55
63
  if (!onHidePermanently)
56
64
  return '';
65
+ const label = hidePermanentlyLabel ?? HIDE_PERMANENTLY_LABEL.fullWidth;
57
66
  return `
58
67
  <button type="button" class="ncua-notification__action-button ncua-notification__action-button--text ncua-full-width-notification__link" data-hide-permanently="true">
59
- 다시보지 않기
68
+ ${label}
60
69
  </button>
61
70
  `;
62
71
  }
@@ -18,7 +18,7 @@ export class MessageNotification {
18
18
  this.setupAutoClose();
19
19
  }
20
20
  createElement() {
21
- const { title, supportingText, color, className, actions, onClose, onHidePermanently } = this.options;
21
+ const { title, supportingText, color, className, actions, onClose, onHidePermanently, hidePermanentlyLabel } = this.options;
22
22
  // message 타입은 neutral, error, warning, success 4가지 색상만 지원
23
23
  let actualColor = color;
24
24
  if (color === 'info') {
@@ -47,6 +47,7 @@ export class MessageNotification {
47
47
  actions,
48
48
  onClose,
49
49
  onHidePermanently,
50
+ hidePermanentlyLabel,
50
51
  }));
51
52
  // FeaturedIcon을 content-wrapper에 추가
52
53
  if (featuredIconElement) {
@@ -33,5 +33,15 @@ export interface BaseNotificationOptions {
33
33
  autoClose?: number;
34
34
  supportTextLink?: string;
35
35
  onHidePermanently?: () => void;
36
+ /**
37
+ * 다시보지 않기 버튼의 라벨. full-width, message 타입에서 사용 가능하며
38
+ * `onHidePermanently`를 지정했을 때만 렌더된다.
39
+ *
40
+ * 기본값은 타입별로 다르다 — message는 `'다시 보지 않기'`, full-width는 `'다시보지 않기'`.
41
+ * 표기가 통일되어 있지 않은 것은 현행 유지이며, 통일은 별도 결정 사항이다.
42
+ *
43
+ * 값은 HTML에 삽입되기 전 `sanitizeHtml`을 거친다.
44
+ */
45
+ hidePermanentlyLabel?: string;
36
46
  }
37
47
  export declare const MESSAGE_CLOSE_ICON_COLORS: Record<NotificationColor, string>;
@@ -5,6 +5,7 @@ export interface MessageTemplateParams {
5
5
  actions?: NotificationAction[];
6
6
  onClose?: () => void;
7
7
  onHidePermanently?: () => void;
8
+ hidePermanentlyLabel?: string;
8
9
  }
9
10
  /**
10
11
  * Message Notification의 마크업을 만든다.
@@ -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
  /**
@@ -11,7 +12,7 @@ const ACTION_BUTTON_SIZE = 'xs';
11
12
  * 동일한 클래스 트리를 산출해야 한다 — SCSS 한 벌을 두 구현이 공유한다.
12
13
  */
13
14
  export function buildMessageTemplate(params) {
14
- const { title, supportingText, actions, onClose, onHidePermanently } = params;
15
+ const { title, supportingText, actions, onClose, onHidePermanently, hidePermanentlyLabel } = params;
15
16
  return `
16
17
  <div class="${CLASS_NAMES.MESSAGE.CONTAINER}">
17
18
  <div class="${CLASS_NAMES.MESSAGE.CONTENT}">
@@ -21,7 +22,7 @@ export function buildMessageTemplate(params) {
21
22
  ${renderSupportingText(supportingText, CLASS_NAMES.MESSAGE.SUPPORTING_TEXT)}
22
23
  </div>
23
24
  </div>
24
- ${renderActionsContainer(actions, onClose, onHidePermanently)}
25
+ ${renderActionsContainer({ actions, onClose, onHidePermanently, hidePermanentlyLabel })}
25
26
  </div>
26
27
  </div>
27
28
  `;
@@ -31,30 +32,39 @@ export function buildMessageTemplate(params) {
31
32
  * 이 컨테이너의 gap(12px)이 두 그룹 사이 간격을 만든다.
32
33
  * 표시할 요소가 하나도 없으면 빈 컨테이너를 남기지 않는다.
33
34
  */
34
- function renderActionsContainer(actions, onClose, onHidePermanently) {
35
+ function renderActionsContainer(params) {
36
+ const { actions, onClose, onHidePermanently, hidePermanentlyLabel } = params;
35
37
  const actionsHtml = renderActions(actions ?? [], CLASS_NAMES.MESSAGE.ACTIONS, ACTION_BUTTON_SIZE);
36
- const footerHtml = renderFooterContainer(onClose, onHidePermanently);
38
+ const footerHtml = renderFooterContainer({ onClose, onHidePermanently, hidePermanentlyLabel });
37
39
  if (!actionsHtml && !footerHtml)
38
40
  return '';
39
41
  return `<div class="${CLASS_NAMES.MESSAGE.ACTIONS_CONTAINER}">${actionsHtml}${footerHtml}</div>`;
40
42
  }
41
43
  /** 다시 보지 않기 + 닫기 그룹. 전용 래퍼를 쓰는 이유는 `CLASS_NAMES.MESSAGE.FOOTER_CONTAINER` 참조. */
42
- function renderFooterContainer(onClose, onHidePermanently) {
44
+ function renderFooterContainer(params) {
45
+ const { onClose, onHidePermanently, hidePermanentlyLabel } = params;
43
46
  if (!onHidePermanently && !onClose)
44
47
  return '';
45
48
  return `
46
49
  <div class="${CLASS_NAMES.MESSAGE.FOOTER_CONTAINER}">
47
- ${renderHidePermanentlyButton(onHidePermanently)}
50
+ ${renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel)}
48
51
  ${renderCloseButton(onClose)}
49
52
  </div>
50
53
  `;
51
54
  }
52
- function renderHidePermanentlyButton(onHidePermanently) {
55
+ /**
56
+ * 다시 보지 않기 버튼.
57
+ *
58
+ * `label`은 이 함수의 반환값이 `wrapper.innerHTML`에 들어가기 전에 `sanitizeHtml`로
59
+ * 처리되므로 별도 이스케이프하지 않는다 — `renderSupportingText`와 같은 정책이다.
60
+ */
61
+ function renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel) {
53
62
  if (!onHidePermanently)
54
63
  return '';
64
+ const label = hidePermanentlyLabel ?? HIDE_PERMANENTLY_LABEL.message;
55
65
  return `
56
66
  <button type="button" class="${CLASS_NAMES.COMMON.ACTION_BUTTON} ${CLASS_NAMES.MESSAGE.HIDE_LINK}" data-hide-permanently="true">
57
- 다시 보지 않기
67
+ ${label}
58
68
  </button>
59
69
  `;
60
70
  }
@@ -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 declare const HIDE_PERMANENTLY_LABEL: {
13
+ readonly message: "다시 보지 않기";
14
+ readonly fullWidth: "다시보지 않기";
15
+ };
@@ -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
+ };
@@ -40,6 +40,12 @@ interface FullWidthNotificationProps extends Omit<ComponentPropsWithoutRef<'div'
40
40
  * 다시보지 않기 이벤트 핸들러 (선택사항)
41
41
  */
42
42
  onHidePermanently?: () => void;
43
+ /**
44
+ * 다시보지 않기 버튼의 라벨 (선택사항)
45
+ * `onHidePermanently`를 지정했을 때만 렌더된다.
46
+ * @default '다시보지 않기'
47
+ */
48
+ hidePermanentlyLabel?: ReactNode;
43
49
  }
44
50
  declare const FullWidthNotification: import("react").ForwardRefExoticComponent<FullWidthNotificationProps & import("react").RefAttributes<HTMLDivElement>>;
45
51
  export type { FullWidthNotificationProps };
@@ -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,
@@ -17,7 +18,7 @@ const iconColorMap = {
17
18
  success: 'green600',
18
19
  info: 'violet600',
19
20
  };
20
- const FullWidthNotification = forwardRef(({ title, supportingText, color = 'neutral', onClose, className, actions, autoClose = 0, supportTextLink, onHidePermanently, ...rest }, ref) => {
21
+ const FullWidthNotification = forwardRef(({ title, supportingText, color = 'neutral', onClose, className, actions, autoClose = 0, supportTextLink, onHidePermanently, hidePermanentlyLabel = HIDE_PERMANENTLY_LABEL.fullWidth, ...rest }, ref) => {
21
22
  const [shouldRemove, setShouldRemove] = useState(false);
22
23
  const closeIconSize = {
23
24
  width: 20,
@@ -51,6 +52,6 @@ const FullWidthNotification = forwardRef(({ title, supportingText, color = 'neut
51
52
  return (_jsx("div", { ref: ref, className: classNames('ncua-full-width-notification', `ncua-full-width-notification--${color}`, className), role: "alert", ...rest, children: _jsx("div", { className: "ncua-full-width-notification__container", children: _jsxs("div", { className: "ncua-full-width-notification__content", children: [_jsxs("div", { className: "ncua-full-width-notification__content-wrapper", children: [(() => {
52
53
  const IconComponent = iconMap[color];
53
54
  return (_jsx(IconComponent, { width: 16, height: 16, color: COLOR[iconColorMap[color]], className: "ncua-full-width-notification__icon" }));
54
- })(), _jsxs("div", { className: "ncua-full-width-notification__text-container", children: [_jsx("span", { className: "ncua-full-width-notification__title", children: title }), !supportTextLink && supportingText && (_jsx("span", { className: "ncua-full-width-notification__supporting-text", children: supportingText })), supportTextLink && supportingText && (_jsx("a", { href: supportTextLink, className: "ncua-full-width-notification__link", children: _jsx("span", { className: "ncua-full-width-notification__supporting-text", children: supportingText }) }))] })] }), _jsxs("div", { className: "ncua-full-width-notification__actions-container", children: [actions && (_jsx("div", { className: "ncua-full-width-notification__actions", children: actions.map((action) => (_jsx("button", { onClick: action.onClick, className: classNames('ncua-notification__action-button', `ncua-notification__action-button--${action.hierarchy || 'text'}`), children: action.label }, `${action.label}-${action.hierarchy}`))) })), onHidePermanently && (_jsx("button", { type: "button", className: classNames('ncua-notification__action-button', 'ncua-notification__action-button--text', 'ncua-full-width-notification__link'), onClick: onHidePermanently, children: "\uB2E4\uC2DC\uBCF4\uC9C0 \uC54A\uAE30" })), onClose && (_jsx("button", { type: "button", className: "ncua-full-width-notification__close-button", onClick: onClose, "aria-label": "\uC54C\uB9BC \uB2EB\uAE30", children: _jsx(XClose, { ...closeIconSize, color: COLOR[iconColorMap[color]] }) }))] })] }) }) }));
55
+ })(), _jsxs("div", { className: "ncua-full-width-notification__text-container", children: [_jsx("span", { className: "ncua-full-width-notification__title", children: title }), !supportTextLink && supportingText && (_jsx("span", { className: "ncua-full-width-notification__supporting-text", children: supportingText })), supportTextLink && supportingText && (_jsx("a", { href: supportTextLink, className: "ncua-full-width-notification__link", children: _jsx("span", { className: "ncua-full-width-notification__supporting-text", children: supportingText }) }))] })] }), _jsxs("div", { className: "ncua-full-width-notification__actions-container", children: [actions && (_jsx("div", { className: "ncua-full-width-notification__actions", children: actions.map((action) => (_jsx("button", { onClick: action.onClick, className: classNames('ncua-notification__action-button', `ncua-notification__action-button--${action.hierarchy || 'text'}`), children: action.label }, `${action.label}-${action.hierarchy}`))) })), onHidePermanently && (_jsx("button", { type: "button", className: classNames('ncua-notification__action-button', 'ncua-notification__action-button--text', 'ncua-full-width-notification__link'), onClick: onHidePermanently, children: hidePermanentlyLabel })), onClose && (_jsx("button", { type: "button", className: "ncua-full-width-notification__close-button", onClick: onClose, "aria-label": "\uC54C\uB9BC \uB2EB\uAE30", children: _jsx(XClose, { ...closeIconSize, color: COLOR[iconColorMap[color]] }) }))] })] }) }) }));
55
56
  });
56
57
  export { FullWidthNotification };
@@ -35,6 +35,12 @@ interface MessageNotificationProps extends Omit<ComponentPropsWithoutRef<'div'>,
35
35
  * 다시보지 않기 이벤트 핸들러 (선택사항)
36
36
  */
37
37
  onHidePermanently?: () => void;
38
+ /**
39
+ * 다시보지 않기 버튼의 라벨 (선택사항)
40
+ * `onHidePermanently`를 지정했을 때만 렌더된다.
41
+ * @default '다시 보지 않기'
42
+ */
43
+ hidePermanentlyLabel?: ReactNode;
38
44
  }
39
45
  declare const MessageNotification: import("react").ForwardRefExoticComponent<MessageNotificationProps & import("react").RefAttributes<HTMLDivElement>>;
40
46
  export type { MessageNotificationProps };
@@ -16,10 +16,10 @@ const iconColorMap = {
16
16
  warning: 'orange500',
17
17
  success: 'green600',
18
18
  };
19
- const MessageNotification = forwardRef(({ title, supportingText, icon: Icon, color = 'neutral', onClose, className, actions, onHidePermanently, ...rest }, ref) => {
19
+ const MessageNotification = forwardRef(({ title, supportingText, icon: Icon, color = 'neutral', onClose, className, actions, onHidePermanently, hidePermanentlyLabel, ...rest }, ref) => {
20
20
  // message 타입은 neutral, error, warning, success 4가지 색상만 지원
21
21
  const validColor = color === 'info' ? 'neutral' : color;
22
- return (_jsx("div", { ref: ref, className: classNames('ncua-message-notification', `ncua-message-notification--${validColor}`, className), role: "alert", ...rest, children: _jsx("div", { className: "ncua-message-notification__container", children: _jsxs("div", { className: "ncua-message-notification__content", children: [_jsx(MessageNotificationContent, { title: title, supportingText: supportingText, icon: Icon || iconMap[validColor], iconColor: validColor, showIcon: Boolean(iconMap[validColor]) }), _jsx(MessageNotificationActions, { actions: actions, onClose: onClose, onHidePermanently: onHidePermanently, closeIconColor: COLOR[iconColorMap[validColor] || 'gray700'] })] }) }) }));
22
+ return (_jsx("div", { ref: ref, className: classNames('ncua-message-notification', `ncua-message-notification--${validColor}`, className), role: "alert", ...rest, children: _jsx("div", { className: "ncua-message-notification__container", children: _jsxs("div", { className: "ncua-message-notification__content", children: [_jsx(MessageNotificationContent, { title: title, supportingText: supportingText, icon: Icon || iconMap[validColor], iconColor: validColor, showIcon: Boolean(iconMap[validColor]) }), _jsx(MessageNotificationActions, { actions: actions, onClose: onClose, onHidePermanently: onHidePermanently, hidePermanentlyLabel: hidePermanentlyLabel, closeIconColor: COLOR[iconColorMap[validColor] || 'gray700'] })] }) }) }));
23
23
  });
24
24
  MessageNotification.displayName = 'MessageNotification';
25
25
  export { MessageNotification };
@@ -15,6 +15,7 @@ interface MessageNotificationActionsProps {
15
15
  actions?: NotificationAction[];
16
16
  onClose?: () => void;
17
17
  onHidePermanently?: () => void;
18
+ hidePermanentlyLabel?: ReactNode;
18
19
  closeIconColor: string;
19
20
  }
20
21
  /**
@@ -22,6 +23,6 @@ interface MessageNotificationActionsProps {
22
23
  * 이 컨테이너의 gap(12px)이 두 그룹 사이 간격을 만든다.
23
24
  * 표시할 요소가 하나도 없으면 빈 컨테이너를 남기지 않는다.
24
25
  */
25
- declare const MessageNotificationActions: ({ actions, onClose, onHidePermanently, closeIconColor, }: MessageNotificationActionsProps) => import("react/jsx-runtime").JSX.Element | null;
26
+ declare const MessageNotificationActions: ({ actions, onClose, onHidePermanently, hidePermanentlyLabel, closeIconColor, }: MessageNotificationActionsProps) => import("react/jsx-runtime").JSX.Element | null;
26
27
  export type { MessageNotificationActionsProps, MessageNotificationContentProps };
27
28
  export { MessageNotificationActions, MessageNotificationContent };
@@ -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
  /** 아이콘 + 제목/서포팅 텍스트 영역. */
@@ -17,11 +18,11 @@ const MessageNotificationContent = ({ title, supportingText, icon, iconColor, sh
17
18
  * 이 컨테이너의 gap(12px)이 두 그룹 사이 간격을 만든다.
18
19
  * 표시할 요소가 하나도 없으면 빈 컨테이너를 남기지 않는다.
19
20
  */
20
- const MessageNotificationActions = ({ actions, onClose, onHidePermanently, closeIconColor, }) => {
21
+ const MessageNotificationActions = ({ actions, onClose, onHidePermanently, hidePermanentlyLabel = HIDE_PERMANENTLY_LABEL.message, closeIconColor, }) => {
21
22
  const hasActions = Boolean(actions?.length);
22
23
  const hasFooter = Boolean(onHidePermanently || onClose);
23
24
  if (!hasActions && !hasFooter)
24
25
  return null;
25
- return (_jsxs("div", { className: "ncua-message-notification__actions-container", children: [hasActions && (_jsx("div", { className: "ncua-message-notification__actions", children: actions?.map((action) => (_jsx(Button, { size: "xs", hierarchy: action.hierarchy || 'text', label: action.label, onClick: action?.onClick }, `${action.label}-${action.hierarchy}`))) })), hasFooter && (_jsxs("div", { className: "ncua-message-notification__footer-container", children: [onHidePermanently && (_jsx("button", { type: "button", className: "ncua-notification__action-button ncua-message-notification__hide-link", onClick: onHidePermanently, children: "\uB2E4\uC2DC \uBCF4\uC9C0 \uC54A\uAE30" })), onClose && (_jsx("button", { type: "button", className: "ncua-message-notification__close-button", onClick: onClose, "aria-label": "\uC54C\uB9BC \uB2EB\uAE30", children: _jsx(XClose, { width: 20, height: 20, color: closeIconColor }) }))] }))] }));
26
+ return (_jsxs("div", { className: "ncua-message-notification__actions-container", children: [hasActions && (_jsx("div", { className: "ncua-message-notification__actions", children: actions?.map((action) => (_jsx(Button, { size: "xs", hierarchy: action.hierarchy || 'text', label: action.label, onClick: action?.onClick }, `${action.label}-${action.hierarchy}`))) })), hasFooter && (_jsxs("div", { className: "ncua-message-notification__footer-container", children: [onHidePermanently && (_jsx("button", { type: "button", className: "ncua-notification__action-button ncua-message-notification__hide-link", onClick: onHidePermanently, children: hidePermanentlyLabel })), onClose && (_jsx("button", { type: "button", className: "ncua-message-notification__close-button", onClick: onClose, "aria-label": "\uC54C\uB9BC \uB2EB\uAE30", children: _jsx(XClose, { width: 20, height: 20, color: closeIconColor }) }))] }))] }));
26
27
  };
27
28
  export { MessageNotificationActions, MessageNotificationContent };
@@ -68,6 +68,14 @@ interface NotificationProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title
68
68
  * message, full-width 타입에서 사용 가능
69
69
  */
70
70
  onHidePermanently?: () => void;
71
+ /**
72
+ * 다시보지 않기 버튼의 라벨 (선택사항)
73
+ * message, full-width 타입에서 사용 가능하며 `onHidePermanently`를 지정했을 때만 렌더된다.
74
+ *
75
+ * 기본값은 타입별로 다르다 — message는 `'다시 보지 않기'`, full-width는 `'다시보지 않기'`.
76
+ * 표기가 통일되어 있지 않은 것은 현행 유지이며, 통일은 별도 결정 사항이다.
77
+ */
78
+ hidePermanentlyLabel?: ReactNode;
71
79
  /**
72
80
  * Portal 마운트 여부. `floating` 타입에서만 동작한다.
73
81
  *
@@ -0,0 +1,160 @@
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({ root, container });
32
+ return container;
33
+ }
34
+ function renderMessage(props = {}) {
35
+ return render(createElement(MessageNotification, { title: '알림 제목', ...props }));
36
+ }
37
+ function renderFullWidth(props = {}) {
38
+ return render(createElement(FullWidthNotification, { title: '알림 제목', ...props }));
39
+ }
40
+ function renderNotification(props = {}) {
41
+ return render(createElement(Notification, { title: '알림 제목', ...props }));
42
+ }
43
+ afterEach(() => {
44
+ for (const { root, container } of mounted.splice(0)) {
45
+ act(() => {
46
+ root.unmount();
47
+ });
48
+ container.remove();
49
+ }
50
+ });
51
+ describe('hidePermanentlyLabel (React) — message 타입', () => {
52
+ it('R-1: prop 미지정 시 기본 문구 "다시 보지 않기"가 렌더된다', () => {
53
+ // Given: hidePermanentlyLabel 없이 onHidePermanently만 주어지고
54
+ // When: MessageNotification을 렌더하면
55
+ const container = renderMessage({ onHidePermanently: vi.fn() });
56
+ // Then: 현행 기본 문구가 그대로 노출된다 (시각적 변화 0)
57
+ const hideLink = container.querySelector(SELECTOR.MESSAGE_HIDE_LINK);
58
+ expect(hideLink?.textContent).toBe(DEFAULT_LABEL.MESSAGE);
59
+ });
60
+ it('R-2: prop 지정 시 그 값이 렌더된다', () => {
61
+ // Given: 커스텀 라벨이 주어지고
62
+ // When: 렌더하면
63
+ const container = renderMessage({ onHidePermanently: vi.fn(), hidePermanentlyLabel: '30일간 보지 않기' });
64
+ // Then: 하드코딩 문구 대신 전달한 값이 노출된다
65
+ const hideLink = container.querySelector(SELECTOR.MESSAGE_HIDE_LINK);
66
+ expect(hideLink?.textContent).toBe('30일간 보지 않기');
67
+ });
68
+ it('R-3: ReactNode 라벨을 받아 엘리먼트로 렌더한다', () => {
69
+ // Given: 문자열이 아닌 ReactNode 라벨이 주어지고
70
+ const label = createElement('span', { className: 'custom-hide-label' }, '커스텀 노드');
71
+ // When: 렌더하면
72
+ const container = renderMessage({ onHidePermanently: vi.fn(), hidePermanentlyLabel: label });
73
+ // Then: 노드가 hide-link 내부에 그대로 마운트된다
74
+ const hideLink = container.querySelector(SELECTOR.MESSAGE_HIDE_LINK);
75
+ expect(hideLink?.querySelector('.custom-hide-label')?.textContent).toBe('커스텀 노드');
76
+ });
77
+ it('R-4: 라벨을 바꿔도 __hide-link 클래스 계약이 유지된다', () => {
78
+ // Given: 커스텀 라벨이 주어지고
79
+ // When: 렌더하면
80
+ const container = renderMessage({ onHidePermanently: vi.fn(), hidePermanentlyLabel: '커스텀' });
81
+ // Then: 고도몰의 textContent 덮어쓰기가 계속 동작하도록 기존 클래스가 모두 남는다
82
+ const hideLink = container.querySelector(SELECTOR.MESSAGE_HIDE_LINK);
83
+ expect(hideLink).not.toBeNull();
84
+ expect(hideLink?.classList.contains('ncua-notification__action-button')).toBe(true);
85
+ expect(hideLink?.classList.contains('ncua-message-notification__hide-link')).toBe(true);
86
+ });
87
+ it('R-5: onHidePermanently 없이 라벨만 주면 버튼이 렌더되지 않는다', () => {
88
+ // Given: 핸들러 없이 라벨만 주어지고
89
+ // When: 렌더하면
90
+ const container = renderMessage({ hidePermanentlyLabel: '커스텀' });
91
+ // Then: 라벨만으로는 버튼이 생기지 않는다 (핸들러가 렌더 조건)
92
+ expect(container.querySelector(SELECTOR.MESSAGE_HIDE_LINK)).toBeNull();
93
+ });
94
+ it('R-6: 라벨이 DOM 속성으로 새어나가지 않는다', () => {
95
+ // Given: 커스텀 라벨이 주어지고
96
+ // When: 렌더하면
97
+ const container = renderMessage({ onHidePermanently: vi.fn(), hidePermanentlyLabel: '커스텀' });
98
+ // Then: rest 스프레드를 타고 루트 div의 속성으로 흘러들지 않는다
99
+ const root = container.querySelector('.ncua-message-notification');
100
+ expect(root?.hasAttribute('hidepermanentlylabel')).toBe(false);
101
+ });
102
+ });
103
+ describe('hidePermanentlyLabel (React) — full-width 타입', () => {
104
+ it('R-7: prop 미지정 시 기본 문구 "다시보지 않기"가 렌더된다', () => {
105
+ // Given: hidePermanentlyLabel 없이 onHidePermanently만 주어지고
106
+ // When: FullWidthNotification을 렌더하면
107
+ const container = renderFullWidth({ onHidePermanently: vi.fn() });
108
+ // Then: message 타입과 다른 현행 문구가 그대로 유지된다 (표기 통일은 별도 결정)
109
+ const hideButton = container.querySelector(SELECTOR.FULL_WIDTH_LINK);
110
+ expect(hideButton?.textContent).toBe(DEFAULT_LABEL.FULL_WIDTH);
111
+ });
112
+ it('R-8: prop 지정 시 그 값이 렌더된다', () => {
113
+ // Given: 커스텀 라벨이 주어지고
114
+ // When: 렌더하면
115
+ const container = renderFullWidth({ onHidePermanently: vi.fn(), hidePermanentlyLabel: '오늘 하루 보지 않기' });
116
+ // Then: 전달한 값이 노출된다
117
+ const hideButton = container.querySelector(SELECTOR.FULL_WIDTH_LINK);
118
+ expect(hideButton?.textContent).toBe('오늘 하루 보지 않기');
119
+ });
120
+ it('R-9: 라벨을 바꿔도 기존 클래스 계약이 유지된다', () => {
121
+ // Given: 커스텀 라벨이 주어지고
122
+ // When: 렌더하면
123
+ const container = renderFullWidth({ onHidePermanently: vi.fn(), hidePermanentlyLabel: '커스텀' });
124
+ // Then: 액션 버튼 클래스가 그대로 남는다
125
+ const hideButton = container.querySelector(SELECTOR.FULL_WIDTH_LINK);
126
+ expect(hideButton?.classList.contains('ncua-notification__action-button')).toBe(true);
127
+ expect(hideButton?.classList.contains('ncua-notification__action-button--text')).toBe(true);
128
+ });
129
+ it('R-10: onHidePermanently 없이 라벨만 주면 버튼이 렌더되지 않는다', () => {
130
+ // Given: 핸들러 없이 라벨만 주어지고
131
+ // When: 렌더하면
132
+ const container = renderFullWidth({ hidePermanentlyLabel: '커스텀' });
133
+ // Then: 액션 버튼이 생기지 않는다
134
+ expect(container.querySelector(SELECTOR.ACTION_BUTTON)).toBeNull();
135
+ });
136
+ });
137
+ describe('hidePermanentlyLabel (React) — Notification 공유 Props 경유', () => {
138
+ it('R-11: type="message"에서 공유 Props의 라벨이 전달된다', () => {
139
+ // Given: 공유 Notification props로 라벨이 주어지고
140
+ // When: type="message"로 렌더하면
141
+ const container = renderNotification({
142
+ type: 'message',
143
+ onHidePermanently: vi.fn(),
144
+ hidePermanentlyLabel: '공유 프롭 라벨',
145
+ });
146
+ // Then: message 구현까지 라벨이 도달한다
147
+ expect(container.querySelector(SELECTOR.MESSAGE_HIDE_LINK)?.textContent).toBe('공유 프롭 라벨');
148
+ });
149
+ it('R-12: type="full-width"에서 공유 Props의 라벨이 전달된다', () => {
150
+ // Given: 공유 Notification props로 라벨이 주어지고
151
+ // When: type="full-width"로 렌더하면
152
+ const container = renderNotification({
153
+ type: 'full-width',
154
+ onHidePermanently: vi.fn(),
155
+ hidePermanentlyLabel: '공유 프롭 라벨',
156
+ });
157
+ // Then: full-width 구현까지 라벨이 도달한다
158
+ expect(container.querySelector(SELECTOR.FULL_WIDTH_LINK)?.textContent).toBe('공유 프롭 라벨');
159
+ });
160
+ });