@ncds/ui-admin 1.8.26-alpha.7 → 1.8.26-alpha.9

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 (87) hide show
  1. package/dist/cjs/assets/scripts/notification/FloatingNotification.js +4 -1
  2. package/dist/cjs/assets/scripts/notification/FullWidthNotification.js +5 -1
  3. package/dist/cjs/assets/scripts/notification/MessageNotification.js +7 -16
  4. package/dist/cjs/assets/scripts/notification/const/classNames.js +0 -2
  5. package/dist/cjs/assets/scripts/notification/const/icons.js +1 -11
  6. package/dist/cjs/assets/scripts/notification/const/index.js +0 -12
  7. package/dist/cjs/assets/scripts/notification/const/sizes.js +1 -6
  8. package/dist/cjs/assets/scripts/notification/messageTemplate.js +4 -2
  9. package/dist/cjs/assets/scripts/notification/utils.js +13 -5
  10. package/dist/cjs/src/components/navigation/pagination/Pagination.js +14 -14
  11. package/dist/cjs/src/components/navigation/pagination/__tests__/Pagination.test.js +17 -0
  12. package/dist/cjs/src/components/overlays/notification/MessageNotification.js +4 -19
  13. package/dist/cjs/src/components/overlays/notification/MessageNotificationParts.js +7 -18
  14. package/dist/cjs/src/components/overlays/notification/Notification.js +6 -5
  15. package/dist/cjs/src/components/overlays/notification/__tests__/MessageNotification.test.js +7 -7
  16. package/dist/cjs/src/components/overlays/notification/__tests__/Notification.test.js +8 -0
  17. package/dist/cjs/src/components/overlays/notification/__tests__/supportTextLink.test.js +1 -7
  18. package/dist/cjs/src/generated/scoped-css.js +1 -1
  19. package/dist/cjs/src/utils/url/isSafeUrl.js +1 -1
  20. package/dist/esm/assets/scripts/notification/FloatingNotification.js +4 -1
  21. package/dist/esm/assets/scripts/notification/FullWidthNotification.js +5 -1
  22. package/dist/esm/assets/scripts/notification/MessageNotification.js +9 -18
  23. package/dist/esm/assets/scripts/notification/const/classNames.js +0 -2
  24. package/dist/esm/assets/scripts/notification/const/icons.js +0 -10
  25. package/dist/esm/assets/scripts/notification/const/index.js +2 -2
  26. package/dist/esm/assets/scripts/notification/const/sizes.js +0 -5
  27. package/dist/esm/assets/scripts/notification/messageTemplate.js +4 -2
  28. package/dist/esm/assets/scripts/notification/utils.js +11 -4
  29. package/dist/esm/src/components/navigation/pagination/Pagination.js +14 -14
  30. package/dist/esm/src/components/navigation/pagination/__tests__/Pagination.test.js +17 -0
  31. package/dist/esm/src/components/overlays/notification/MessageNotification.js +5 -20
  32. package/dist/esm/src/components/overlays/notification/MessageNotificationParts.js +7 -18
  33. package/dist/esm/src/components/overlays/notification/Notification.js +6 -5
  34. package/dist/esm/src/components/overlays/notification/__tests__/MessageNotification.test.js +8 -8
  35. package/dist/esm/src/components/overlays/notification/__tests__/Notification.test.js +8 -0
  36. package/dist/esm/src/components/overlays/notification/__tests__/supportTextLink.test.js +1 -7
  37. package/dist/esm/src/generated/scoped-css.js +1 -1
  38. package/dist/esm/src/utils/url/isSafeUrl.js +1 -1
  39. package/dist/temp/assets/scripts/notification/FloatingNotification.js +1 -1
  40. package/dist/temp/assets/scripts/notification/FullWidthNotification.js +1 -1
  41. package/dist/temp/assets/scripts/notification/MessageNotification.d.ts +1 -1
  42. package/dist/temp/assets/scripts/notification/MessageNotification.js +9 -15
  43. package/dist/temp/assets/scripts/notification/const/classNames.d.ts +0 -2
  44. package/dist/temp/assets/scripts/notification/const/classNames.js +0 -2
  45. package/dist/temp/assets/scripts/notification/const/icons.d.ts +0 -2
  46. package/dist/temp/assets/scripts/notification/const/icons.js +0 -10
  47. package/dist/temp/assets/scripts/notification/const/index.d.ts +3 -3
  48. package/dist/temp/assets/scripts/notification/const/index.js +2 -2
  49. package/dist/temp/assets/scripts/notification/const/sizes.d.ts +0 -4
  50. package/dist/temp/assets/scripts/notification/const/sizes.js +0 -5
  51. package/dist/temp/assets/scripts/notification/const/types.d.ts +7 -22
  52. package/dist/temp/assets/scripts/notification/messageTemplate.d.ts +0 -2
  53. package/dist/temp/assets/scripts/notification/messageTemplate.js +2 -2
  54. package/dist/temp/assets/scripts/notification/utils.d.ts +10 -1
  55. package/dist/temp/assets/scripts/notification/utils.js +6 -3
  56. package/dist/temp/src/components/navigation/pagination/Pagination.js +14 -14
  57. package/dist/temp/src/components/navigation/pagination/__tests__/Pagination.test.js +7 -0
  58. package/dist/temp/src/components/overlays/notification/FloatingNotification.d.ts +3 -4
  59. package/dist/temp/src/components/overlays/notification/FullWidthNotification.d.ts +2 -4
  60. package/dist/temp/src/components/overlays/notification/MessageNotification.d.ts +2 -18
  61. package/dist/temp/src/components/overlays/notification/MessageNotification.js +4 -8
  62. package/dist/temp/src/components/overlays/notification/MessageNotificationParts.d.ts +4 -10
  63. package/dist/temp/src/components/overlays/notification/MessageNotificationParts.js +4 -7
  64. package/dist/temp/src/components/overlays/notification/Notification.d.ts +8 -28
  65. package/dist/temp/src/components/overlays/notification/Notification.js +4 -2
  66. package/dist/temp/src/components/overlays/notification/__tests__/MessageNotification.test.js +12 -9
  67. package/dist/temp/src/components/overlays/notification/__tests__/Notification.test.js +5 -0
  68. package/dist/temp/src/components/overlays/notification/__tests__/supportTextLink.test.js +0 -11
  69. package/dist/temp/src/generated/scoped-css.js +1 -1
  70. package/dist/temp/src/utils/url/isSafeUrl.d.ts +1 -1
  71. package/dist/temp/src/utils/url/isSafeUrl.js +1 -1
  72. package/dist/types/assets/scripts/notification/MessageNotification.d.ts +1 -1
  73. package/dist/types/assets/scripts/notification/const/classNames.d.ts +0 -2
  74. package/dist/types/assets/scripts/notification/const/icons.d.ts +0 -2
  75. package/dist/types/assets/scripts/notification/const/index.d.ts +3 -3
  76. package/dist/types/assets/scripts/notification/const/sizes.d.ts +0 -4
  77. package/dist/types/assets/scripts/notification/const/types.d.ts +7 -22
  78. package/dist/types/assets/scripts/notification/messageTemplate.d.ts +0 -2
  79. package/dist/types/assets/scripts/notification/utils.d.ts +10 -1
  80. package/dist/types/src/components/overlays/notification/FloatingNotification.d.ts +3 -4
  81. package/dist/types/src/components/overlays/notification/FullWidthNotification.d.ts +2 -4
  82. package/dist/types/src/components/overlays/notification/MessageNotification.d.ts +2 -18
  83. package/dist/types/src/components/overlays/notification/MessageNotificationParts.d.ts +4 -10
  84. package/dist/types/src/components/overlays/notification/Notification.d.ts +8 -28
  85. package/dist/types/src/utils/url/isSafeUrl.d.ts +1 -1
  86. package/dist/ui-admin/assets/styles/style.css +0 -19
  87. package/package.json +1 -1
@@ -19,7 +19,7 @@ function resolveBaseOrigin() {
19
19
  * javascript:, data:, file: 등 위험 프로토콜로 인한 XSS / 외부 페이로드 로딩을 차단.
20
20
  *
21
21
  * URL 파서가 앞뒤 공백·탭·개행 제거와 scheme 소문자화를 하므로 ` JavaScript:` 같은 우회도 막힌다.
22
- * React(`MessageNotificationParts`·`FullWidthNotification`)와 Vanilla(`renderSupportingText`)가 공유한다.
22
+ * React(`FullWidthNotification`)와 Vanilla(`renderSupportingText`)가 공유한다.
23
23
  */
24
24
  function isSafeUrl(url) {
25
25
  if (!url.trim()) return false;
@@ -89,7 +89,10 @@ export class FloatingNotification {
89
89
  <div class="${CLASS_NAMES.FLOATING.TITLE_WRAPPER}">
90
90
  <span class="${CLASS_NAMES.FLOATING.TITLE}">${title}</span>
91
91
  </div>
92
- ${renderSupportingText(supportingText, CLASS_NAMES.FLOATING.SUPPORTING_TEXT)}
92
+ ${renderSupportingText({
93
+ supportingText,
94
+ className: CLASS_NAMES.FLOATING.SUPPORTING_TEXT
95
+ })}
93
96
  ${actions && actions.length > 0 ? renderActions(actions, CLASS_NAMES.FLOATING.ACTIONS) : ''}
94
97
  </div>
95
98
  </div>
@@ -66,7 +66,11 @@ export class FullWidthNotification {
66
66
  <div class="${CLASS_NAMES.FULL_WIDTH.ICON}">${iconHtml}</div>
67
67
  <div class="${CLASS_NAMES.FULL_WIDTH.TEXT_CONTAINER}">
68
68
  <span class="${CLASS_NAMES.FULL_WIDTH.TITLE}">${title}</span>
69
- ${renderSupportingText(supportingText, CLASS_NAMES.FULL_WIDTH.SUPPORTING_TEXT, supportTextLink)}
69
+ ${renderSupportingText({
70
+ supportingText,
71
+ className: CLASS_NAMES.FULL_WIDTH.SUPPORTING_TEXT,
72
+ supportTextLink
73
+ })}
70
74
  </div>
71
75
  </div>
72
76
  <div class="${CLASS_NAMES.FULL_WIDTH.ACTIONS_CONTAINER}">
@@ -1,8 +1,8 @@
1
1
  import { FeaturedIcon } from '../featuredIcon';
2
2
  import { sanitizeHtml } from '../utils/sanitize';
3
- import { CLASS_NAMES, MESSAGE_BLOCK_ICON_MAP, MESSAGE_BLOCK_SIZES, MESSAGE_ICON_MAP, MESSAGE_SIZES } from './const';
3
+ import { CLASS_NAMES, MESSAGE_ICON_MAP, MESSAGE_SIZES } from './const';
4
4
  import { buildMessageTemplate } from './messageTemplate';
5
- import { applySupportTextLinkTarget, bindNotificationEvents, createWrapperElement, setupAutoClose } from './utils';
5
+ import { bindNotificationEvents, createWrapperElement, setupAutoClose } from './utils';
6
6
  export class MessageNotification {
7
7
  constructor(options) {
8
8
  this.options = {
@@ -28,19 +28,13 @@ export class MessageNotification {
28
28
  onClose,
29
29
  hideCloseButton,
30
30
  onHidePermanently,
31
- hidePermanentlyLabel,
32
- variant,
33
- supportTextLink,
34
- supportTextLinkTarget
31
+ hidePermanentlyLabel
35
32
  } = this.options;
36
- const isBlock = variant === 'block';
37
- const wrapperClassName = [isBlock ? CLASS_NAMES.MESSAGE.BLOCK : '', className].filter(Boolean).join(' ');
38
- const wrapper = createWrapperElement(CLASS_NAMES.MESSAGE.BASE, color, wrapperClassName);
39
- const featuredIconElement = this.createFeaturedIcon(color, isBlock);
33
+ const wrapper = createWrapperElement(CLASS_NAMES.MESSAGE.BASE, color, className);
34
+ const featuredIconElement = this.createFeaturedIcon(color);
40
35
  wrapper.innerHTML = sanitizeHtml(buildMessageTemplate({
41
36
  title,
42
37
  supportingText,
43
- supportTextLink: isBlock ? supportTextLink : undefined,
44
38
  color,
45
39
  actions,
46
40
  onClose,
@@ -48,7 +42,6 @@ export class MessageNotification {
48
42
  onHidePermanently,
49
43
  hidePermanentlyLabel
50
44
  }));
51
- applySupportTextLinkTarget(wrapper, CLASS_NAMES.MESSAGE.TEXT_CONTAINER, CLASS_NAMES.MESSAGE.LINK, supportTextLinkTarget);
52
45
  // FeaturedIcon을 content-wrapper에 추가
53
46
  if (featuredIconElement) {
54
47
  const contentWrapper = wrapper.querySelector(`.${CLASS_NAMES.MESSAGE.CONTENT_WRAPPER}`);
@@ -58,16 +51,14 @@ export class MessageNotification {
58
51
  }
59
52
  return wrapper;
60
53
  }
61
- /** 색상별 FeaturedIcon. default 는 md(40/20), block 은 sm(32/16). info 아이콘만 variant 별로 다르다. */
62
- createFeaturedIcon(color, isBlock) {
63
- const iconFunction = (isBlock ? MESSAGE_BLOCK_ICON_MAP : MESSAGE_ICON_MAP)[color];
64
- const sizes = isBlock ? MESSAGE_BLOCK_SIZES : MESSAGE_SIZES;
54
+ /** 색상별 FeaturedIcon. md(40px 원 / 20px 아이콘) 고정. */
55
+ createFeaturedIcon(color) {
65
56
  this.featuredIcon = FeaturedIcon.create({
66
- svgString: iconFunction(sizes.ICON_PIXEL),
57
+ svgString: MESSAGE_ICON_MAP[color](MESSAGE_SIZES.ICON_PIXEL),
67
58
  theme: 'light-circle',
68
59
  // FeaturedIcon 에는 info 색이 없다. info 의 violet 톤은 message SCSS 가 덮어쓴다.
69
60
  color: color === 'info' ? 'neutral' : color,
70
- size: sizes.FEATURED_ICON,
61
+ size: MESSAGE_SIZES.FEATURED_ICON,
71
62
  // React 구현과 동일하게 `__icon`(flex-shrink: 0)을 부여한다.
72
63
  // 누락 시 긴 타이틀에서 아이콘이 찌그러진다.
73
64
  className: CLASS_NAMES.MESSAGE.ICON
@@ -27,7 +27,6 @@ export const CLASS_NAMES = {
27
27
  },
28
28
  MESSAGE: {
29
29
  BASE: 'ncua-message-notification',
30
- BLOCK: 'ncua-message-notification--block',
31
30
  CONTAINER: 'ncua-message-notification__container',
32
31
  CONTENT: 'ncua-message-notification__content',
33
32
  CONTENT_WRAPPER: 'ncua-message-notification__content-wrapper',
@@ -35,7 +34,6 @@ export const CLASS_NAMES = {
35
34
  TEXT_CONTAINER: 'ncua-message-notification__text-container',
36
35
  TITLE: 'ncua-message-notification__title',
37
36
  SUPPORTING_TEXT: 'ncua-message-notification__supporting-text',
38
- LINK: 'ncua-message-notification__link',
39
37
  ACTIONS_CONTAINER: 'ncua-message-notification__actions-container',
40
38
  ACTIONS: 'ncua-message-notification__actions',
41
39
  // 다시 보지 않기 + 닫기 그룹 전용 래퍼. ACTIONS_CONTAINER를 재사용하면
@@ -1,8 +1,3 @@
1
- /**
2
- * message block 변형의 InfoCircle. `SVG_ICONS['info-circle']`은 full-width 전용 class·color 가 박혀 있어
3
- * 쓰지 않는다 (`.ncua-full-width-notification__icon`은 모바일에서 숨겨진다).
4
- */
5
- const INFO_CIRCLE_ICON = size => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 16v-4m0-4h.01M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10"></path></svg>`;
6
1
  // SVG 아이콘들 (크기는 동적으로 설정)
7
2
  export const SVG_ICONS = {
8
3
  'pin-02': size => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.377 15.616 2.72 21.273m8.974-14.631-1.56 1.56a2 2 0 0 1-.264.242 1 1 0 0 1-.207.111c-.082.032-.17.05-.347.085l-3.665.733c-.952.19-1.428.286-1.65.537a1 1 0 0 0-.243.8c.046.333.39.677 1.076 1.363l7.086 7.086c.686.687 1.03 1.03 1.362 1.076a1 1 0 0 0 .801-.242c.251-.223.346-.7.537-1.651l.733-3.665c.035-.176.053-.265.085-.347a1 1 0 0 1 .11-.207c.051-.072.115-.136.242-.263l1.561-1.561c.082-.082.122-.122.167-.158q.06-.047.126-.085c.05-.029.103-.051.208-.097l2.495-1.069c.727-.312 1.091-.467 1.256-.72a1 1 0 0 0 .144-.747c-.06-.295-.34-.575-.9-1.135l-5.142-5.143c-.56-.56-.84-.84-1.135-.9a1 1 0 0 0-.748.145c-.252.165-.407.529-.72 1.256l-1.068 2.495a2 2 0 0 1-.097.208 1 1 0 0 1-.085.126 2 2 0 0 1-.158.167"></path></svg>`,
@@ -35,9 +30,4 @@ export const MESSAGE_ICON_MAP = {
35
30
  warning: SVG_ICONS['alert-circle'],
36
31
  success: SVG_ICONS['check-circle'],
37
32
  info: SVG_ICONS['pin-02']
38
- };
39
- /** block 변형 아이콘. info 만 InfoCircle 로 다르다 (#536 유지). */
40
- export const MESSAGE_BLOCK_ICON_MAP = {
41
- ...MESSAGE_ICON_MAP,
42
- info: INFO_CIRCLE_ICON
43
33
  };
@@ -1,4 +1,4 @@
1
1
  // Re-export all constants and types from separate files
2
2
  export { CLASS_NAMES } from './classNames';
3
- export { FLOATING_ICON_MAP, FULL_WIDTH_ICON_MAP, ICON_MAP, MESSAGE_BLOCK_ICON_MAP, MESSAGE_ICON_MAP, SVG_ICONS } from './icons';
4
- export { CLOSE_BUTTON_SIZES, CLOSE_BUTTON_SVG_SIZES, FEATURED_ICON_SIZES, FULL_WIDTH_SIZES, getSizes, ICON_PIXEL_SIZES, MESSAGE_BLOCK_SIZES, MESSAGE_SIZES } from './sizes';
3
+ export { FLOATING_ICON_MAP, FULL_WIDTH_ICON_MAP, ICON_MAP, MESSAGE_ICON_MAP, SVG_ICONS } from './icons';
4
+ export { CLOSE_BUTTON_SIZES, CLOSE_BUTTON_SVG_SIZES, FEATURED_ICON_SIZES, FULL_WIDTH_SIZES, getSizes, ICON_PIXEL_SIZES, MESSAGE_SIZES } from './sizes';
@@ -33,11 +33,6 @@ export const MESSAGE_SIZES = {
33
33
  ICON_PIXEL: '20',
34
34
  CLOSE_BUTTON: '20'
35
35
  };
36
- // Message 알림 Block 변형 사이즈. 'sm' = 32px 원 / 16px 아이콘. 닫기 버튼은 default 와 같다.
37
- export const MESSAGE_BLOCK_SIZES = {
38
- FEATURED_ICON: 'sm',
39
- ICON_PIXEL: '16'
40
- };
41
36
  // 사이즈 유틸리티 함수들
42
37
  export const getSizes = {
43
38
  featuredIcon: isMobile => isMobile ? FEATURED_ICON_SIZES.MOBILE : FEATURED_ICON_SIZES.DESKTOP,
@@ -15,7 +15,6 @@ export function buildMessageTemplate(params) {
15
15
  const {
16
16
  title,
17
17
  supportingText,
18
- supportTextLink,
19
18
  color,
20
19
  actions,
21
20
  onClose,
@@ -29,7 +28,10 @@ export function buildMessageTemplate(params) {
29
28
  <div class="${CLASS_NAMES.MESSAGE.CONTENT_WRAPPER}">
30
29
  <div class="${CLASS_NAMES.MESSAGE.TEXT_CONTAINER}">
31
30
  <span class="${CLASS_NAMES.MESSAGE.TITLE}">${title}</span>
32
- ${renderSupportingText(supportingText, CLASS_NAMES.MESSAGE.SUPPORTING_TEXT, supportTextLink, CLASS_NAMES.MESSAGE.LINK)}
31
+ ${renderSupportingText({
32
+ supportingText,
33
+ className: CLASS_NAMES.MESSAGE.SUPPORTING_TEXT
34
+ })}
33
35
  </div>
34
36
  </div>
35
37
  ${renderActionsContainer({
@@ -22,19 +22,26 @@ export function buildClassName(baseClass, color, className) {
22
22
  function escapeHtmlAttr(str) {
23
23
  return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
24
24
  }
25
- export function renderSupportingText(supportingText, className, supportTextLink) {
26
- let linkClassName = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : CLASS_NAMES.FULL_WIDTH.LINK;
25
+ /**
26
+ * 보조 텍스트 HTML을 만든다. `supportTextLink` 링크는 full-width 전용 클래스(`CLASS_NAMES.FULL_WIDTH.LINK`)로 렌더한다.
27
+ */
28
+ export const renderSupportingText = _ref => {
29
+ let {
30
+ supportingText,
31
+ className,
32
+ supportTextLink
33
+ } = _ref;
27
34
  if (!supportingText) return '';
28
35
  // 본문(supportingText) 자체는 이 함수의 호출 결과가 wrapper.innerHTML에
29
36
  // 들어가기 전에 DOMPurify로 sanitize되므로 별도 이스케이프하지 않는다.
30
37
  // 이렇게 해야 <br>, <strong> 등 서식용 태그가 그대로 동작한다.
31
38
  if (supportTextLink && isSafeUrl(supportTextLink)) {
32
39
  const safeLink = escapeHtmlAttr(supportTextLink);
33
- return `<a href="${safeLink}" class="${linkClassName}"><span class="${className}">${supportingText}</span></a>`;
40
+ return `<a href="${safeLink}" class="${CLASS_NAMES.FULL_WIDTH.LINK}"><span class="${className}">${supportingText}</span></a>`;
34
41
  }
35
42
  // unsafe URL이거나 link가 없으면 링크 없이 텍스트만 렌더링
36
43
  return `<span class="${className}">${supportingText}</span>`;
37
- }
44
+ };
38
45
  /**
39
46
  * `supportTextLinkTarget`이 `'_blank'`이면 `renderSupportingText`가 만든 링크를 새 탭으로 연다.
40
47
  *
@@ -29,28 +29,28 @@ export const Pagination = _ref => {
29
29
  };
30
30
  useEffect(() => {
31
31
  const lastGroupStart = Math.max(1, Math.floor((totalPage - 1) / pageCount) * pageCount + 1);
32
- // totalPage/pageCount 가 줄어 start 가 마지막 그룹을 넘으면 마지막 그룹으로 보정한다.
32
+ // totalPage/pageCount 가 줄어 start 가 마지막 그룹을 넘으면 마지막 그룹에서 시작해 아래 규칙을 계속 적용한다.
33
33
  // (예: 100→50 페이지로 축소 시 start 가 91 에 머물러 범위 밖 버튼을 표시하던 문제)
34
- if (start > lastGroupStart) {
35
- setStart(lastGroupStart);
36
- return;
34
+ // 바로 return 하면 새 검색으로 currentPage 가 1 이 돼도 1 이 목록에 없다 (PR #603 리뷰).
35
+ const baseStart = Math.min(start, lastGroupStart);
36
+ let nextStart = baseStart;
37
+ if (currentPage === baseStart + pageCount) {
38
+ nextStart = baseStart + pageCount;
37
39
  }
38
- if (currentPage === start + pageCount) {
39
- setStart(start + pageCount);
40
+ if (currentPage > baseStart + pageCount) {
41
+ nextStart = Math.floor((currentPage - 1) / pageCount) * pageCount + 1;
40
42
  }
41
- if (currentPage > start + pageCount) {
42
- const newStart = Math.floor((currentPage - 1) / pageCount) * pageCount + 1;
43
- setStart(newStart);
44
- }
45
- if (currentPage < start) {
46
- setStart(Math.max(1, start - pageCount));
43
+ if (currentPage < baseStart) {
44
+ nextStart = Math.max(1, baseStart - pageCount);
47
45
  }
48
46
  if (currentPage === 1) {
49
- setStart(1);
47
+ nextStart = 1;
50
48
  }
51
49
  if (currentPage === totalPage) {
52
- setStart(lastGroupStart);
50
+ nextStart = lastGroupStart;
53
51
  }
52
+ // currentPage 가 totalPage 를 넘는 값이어도 마지막 그룹을 넘지 않는다
53
+ setStart(Math.min(nextStart, lastGroupStart));
54
54
  // start 는 이 effect 가 setStart 로 갱신하는 대상이라 deps 에서 제외한다(넣으면 자기 갱신으로
55
55
  // 재실행). currentPage 외에 totalPage/pageCount 도 추적해, 이들이 줄어 start 가 범위를 벗어나면
56
56
  // 위에서 보정한다.
@@ -64,6 +64,23 @@ describe('#332 Pagination — totalPage 축소 시 start 보정', () => {
64
64
  expect(nums[0]).toBe(GROUP_START_50_PAGES);
65
65
  expect(Math.max(...nums)).toBeLessThanOrEqual(LAST_PAGE_50);
66
66
  });
67
+ it('totalPage 가 줄면서 currentPage 가 1 로 돌아오면 1 이 든 첫 그룹을 보인다 (PR #603 리뷰)', () => {
68
+ // 50 페이지 중 35 페이지(31~40 그룹) → 새 검색으로 25 페이지 결과의 1 페이지
69
+ const el = render({
70
+ totalCount: 750,
71
+ itemCountPerPage: 15,
72
+ pageCount: PAGE_GROUP_SIZE,
73
+ currentPage: 35
74
+ });
75
+ render({
76
+ totalCount: 375,
77
+ itemCountPerPage: 15,
78
+ pageCount: PAGE_GROUP_SIZE,
79
+ currentPage: 1
80
+ });
81
+ expect(pageNums(el)[0]).toBe(1);
82
+ expect(el.querySelector('.ncua-pagination__page-num.is-current')?.textContent).toBe('1');
83
+ });
67
84
  });
68
85
  describe('Pagination — mo 모드 이전/다음 버튼 노출', () => {
69
86
  it('페이지가 그룹 크기 이하여도 mo 에서는 이전·다음 버튼이 노출된다', () => {
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { AlertCircle, AlertTriangle, CheckCircle, InfoCircle, Pin02 } from '@ncds/ui-admin-icon';
2
+ import { AlertCircle, AlertTriangle, CheckCircle, Pin02 } from '@ncds/ui-admin-icon';
3
3
  import classNames from 'classnames';
4
4
  import { forwardRef } from 'react';
5
5
  import { MESSAGE_CLOSE_ICON_COLOR } from '../../../../constant/notification';
@@ -10,14 +10,9 @@ const DEFAULT_ICON_MAP = {
10
10
  error: AlertTriangle,
11
11
  warning: AlertCircle,
12
12
  success: CheckCircle,
13
- // 디자인 기준 default 의 info 는 neutral 과 같은 핀이다.
13
+ // 디자인 기준 info 는 neutral 과 같은 핀이다.
14
14
  info: Pin02
15
15
  };
16
- // block 의 info 는 InfoCircle 을 유지한다 (#536).
17
- const BLOCK_ICON_MAP = {
18
- ...DEFAULT_ICON_MAP,
19
- info: InfoCircle
20
- };
21
16
  const MessageNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
22
17
  let {
23
18
  title,
@@ -31,13 +26,8 @@ const MessageNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
31
26
  actions,
32
27
  onHidePermanently,
33
28
  hidePermanentlyLabel,
34
- variant = 'default',
35
- supportTextLink,
36
- supportTextLinkTarget,
37
29
  ...rest
38
30
  } = _ref;
39
- const isBlock = variant === 'block';
40
- const iconMap = isBlock ? BLOCK_ICON_MAP : DEFAULT_ICON_MAP;
41
31
  // FeaturedIcon 에는 info 색이 없다. info 의 violet 톤은 message SCSS 가 덮어쓴다.
42
32
  const featuredIconColor = color === 'info' ? 'neutral' : color;
43
33
  // message 는 부모가 언마운트로 닫으므로 hideCloseButton 기본 autoClose 를 적용하지 않는다.
@@ -50,9 +40,7 @@ const MessageNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
50
40
  }
51
41
  return _jsx("div", {
52
42
  ref: ref,
53
- className: classNames('ncua-message-notification', `ncua-message-notification--${color}`, {
54
- 'ncua-message-notification--block': isBlock
55
- }, className),
43
+ className: classNames('ncua-message-notification', `ncua-message-notification--${color}`, className),
56
44
  role: "alert",
57
45
  ...rest,
58
46
  children: _jsx("div", {
@@ -62,11 +50,8 @@ const MessageNotification = /*#__PURE__*/forwardRef((_ref, ref) => {
62
50
  children: [_jsx(MessageNotificationContent, {
63
51
  title: title,
64
52
  supportingText: supportingText,
65
- supportTextLink: isBlock ? supportTextLink : undefined,
66
- supportTextLinkTarget: supportTextLinkTarget,
67
- icon: Icon || iconMap[color],
68
- iconColor: featuredIconColor,
69
- iconSize: isBlock ? 'sm' : 'md'
53
+ icon: Icon || DEFAULT_ICON_MAP[color],
54
+ iconColor: featuredIconColor
70
55
  }), _jsx(MessageNotificationActions, {
71
56
  actions: actions,
72
57
  onClose: onClose,
@@ -1,31 +1,22 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Pin02, XClose } from '@ncds/ui-admin-icon';
3
3
  import { HIDE_PERMANENTLY_LABEL } from '../../../../constant/notification';
4
- import { isSafeUrl } from '../../../utils/url';
5
4
  import { FeaturedIcon } from '../../image-and-icons/featured-icon/FeaturedIcon';
6
- import { getLinkTargetAttributes } from './linkTarget';
7
5
  import { NotificationActionButtons } from './NotificationActionButtons';
8
- /** 아이콘 + 제목/서포팅 텍스트(또는 텍스트 링크) 영역. */
6
+ /** 아이콘 + 제목/서포팅 텍스트 영역. FeaturedIcon 은 md(40px 원 / 20px 아이콘) 고정. */
9
7
  const MessageNotificationContent = _ref => {
10
8
  let {
11
9
  title,
12
10
  supportingText,
13
- supportTextLink,
14
- supportTextLinkTarget,
15
11
  icon,
16
- iconColor,
17
- iconSize
12
+ iconColor
18
13
  } = _ref;
19
14
  const featuredIconProps = {
20
15
  icon: icon || Pin02,
21
- size: iconSize,
16
+ size: 'md',
22
17
  color: iconColor,
23
18
  theme: 'light-circle'
24
19
  };
25
- const supportingTextNode = supportingText && _jsx("span", {
26
- className: "ncua-message-notification__supporting-text",
27
- children: supportingText
28
- });
29
20
  return _jsxs("div", {
30
21
  className: "ncua-message-notification__content-wrapper",
31
22
  children: [_jsx(FeaturedIcon, {
@@ -36,12 +27,10 @@ const MessageNotificationContent = _ref => {
36
27
  children: [_jsx("span", {
37
28
  className: "ncua-message-notification__title",
38
29
  children: title
39
- }), supportTextLink && isSafeUrl(supportTextLink) && supportingTextNode ? _jsx("a", {
40
- href: supportTextLink,
41
- className: "ncua-message-notification__link",
42
- ...getLinkTargetAttributes(supportTextLinkTarget),
43
- children: supportingTextNode
44
- }) : supportingTextNode]
30
+ }), supportingText && _jsx("span", {
31
+ className: "ncua-message-notification__supporting-text",
32
+ children: supportingText
33
+ })]
45
34
  })]
46
35
  });
47
36
  };
@@ -14,9 +14,13 @@ const Notification = /*#__PURE__*/forwardRef(
14
14
  width,
15
15
  supportTextLink,
16
16
  supportTextLinkTarget,
17
- variant,
18
- ...rest
17
+ ...props
19
18
  } = _ref;
19
+ // #580 에서 제거된 message `variant` 가 런타임에 넘어와도 DOM 속성으로 새지 않게 어떤 type 에도 전달하지 않는다.
20
+ const {
21
+ variant: _variant,
22
+ ...rest
23
+ } = props;
20
24
  if (type === 'floating') {
21
25
  return _jsx(FloatingNotification, {
22
26
  color: color,
@@ -36,9 +40,6 @@ const Notification = /*#__PURE__*/forwardRef(
36
40
  if (type === 'message') {
37
41
  return _jsx(MessageNotification, {
38
42
  color: color,
39
- variant: variant,
40
- supportTextLink: supportTextLink,
41
- supportTextLinkTarget: supportTextLinkTarget,
42
43
  ...rest,
43
44
  ref: ref
44
45
  });
@@ -1,5 +1,5 @@
1
1
  // @vitest-environment jsdom
2
- import { InfoCircle, Pin02 } from '@ncds/ui-admin-icon';
2
+ import { Pin02 } from '@ncds/ui-admin-icon';
3
3
  import { createElement } from 'react';
4
4
  import { createRoot } from 'react-dom/client';
5
5
  import { act } from 'react-dom/test-utils';
@@ -156,7 +156,7 @@ describe('MessageNotification (React) — DOM 구조 계약', () => {
156
156
  });
157
157
  describe('MessageNotification (React) — color="info" (#568)', () => {
158
158
  it('R-01: color="info" 는 neutral 로 폴백하지 않고 --info 수식자로 렌더한다', () => {
159
- // Given/When: variant 미지정, info 색상으로 렌더하면
159
+ // Given/When: info 색상으로 렌더하면
160
160
  const root = renderMessageNotification({
161
161
  color: 'info'
162
162
  }).querySelector(SELECTOR.ROOT);
@@ -165,7 +165,7 @@ describe('MessageNotification (React) — color="info" (#568)', () => {
165
165
  expect(root?.classList.contains('ncua-message-notification--neutral')).toBe(false);
166
166
  });
167
167
  it('R-02: 기본 info 아이콘은 neutral 과 같은 Pin02 다', () => {
168
- // Given/When: variant 미지정, info 색상으로 렌더하면
168
+ // Given/When: info 색상으로 렌더하면
169
169
  const container = renderMessageNotification({
170
170
  color: 'info'
171
171
  });
@@ -175,7 +175,7 @@ describe('MessageNotification (React) — color="info" (#568)', () => {
175
175
  expect(renderedIconPathD(container)).toBe(expected);
176
176
  });
177
177
  it('R-03: info 의 FeaturedIcon 은 neutral·md 로 렌더한다 (violet 톤은 SCSS 가 덮는다)', () => {
178
- // Given/When: variant 미지정, info 색상으로 렌더하면
178
+ // Given/When: info 색상으로 렌더하면
179
179
  const icon = renderMessageNotification({
180
180
  color: 'info'
181
181
  }).querySelector(SELECTOR.ICON);
@@ -183,14 +183,14 @@ describe('MessageNotification (React) — color="info" (#568)', () => {
183
183
  expect(icon?.classList.contains('ncua-featured-icon--neutral')).toBe(true);
184
184
  expect(icon?.classList.contains('ncua-featured-icon--md')).toBe(true);
185
185
  });
186
- it('R-04: block 의 info 아이콘은 InfoCircle 을 유지한다', () => {
187
- // Given/When: block 변형에 info 색상으로 렌더하면
186
+ it('R-04: 런타임에 variant="block" 이 넘어와도 info 아이콘은 Pin02 다 (#580 Block 변형 제거)', () => {
187
+ // Given/When: 제거된 variant prop 을 런타임 입력으로 넘겨 info 색상으로 렌더하면
188
188
  const container = renderMessageNotification({
189
189
  variant: 'block',
190
190
  color: 'info'
191
191
  });
192
- // Then: 아이콘 path 가 InfoCircle 과 같다 (#536 회귀)
193
- const expected = iconPathD(InfoCircle);
192
+ // Then: 아이콘 path 가 Pin02 와 같다
193
+ const expected = iconPathD(Pin02);
194
194
  expect(expected).not.toBeNull();
195
195
  expect(renderedIconPathD(container)).toBe(expected);
196
196
  });
@@ -72,6 +72,14 @@ describe('Notification — type 별 위임', () => {
72
72
  });
73
73
  expect(element?.hasAttribute('supporttextlink')).toBe(false);
74
74
  });
75
+ it.each(['floating', 'full-width', 'message'])('type=%s 에 제거된 variant="block" 을 주어도 DOM 속성으로 새지 않는다 (#580)', type => {
76
+ const element = render({
77
+ type,
78
+ variant: 'block'
79
+ });
80
+ expect(element).not.toBeNull();
81
+ expect(element?.hasAttribute('variant')).toBe(false);
82
+ });
75
83
  it('정의되지 않은 type 은 아무것도 렌더하지 않는다', () => {
76
84
  const element = render({
77
85
  type: 'banner'
@@ -4,7 +4,6 @@ import { createRoot } from 'react-dom/client';
4
4
  import { act } from 'react-dom/test-utils';
5
5
  import { afterEach, describe, expect, it } from 'vitest';
6
6
  import { FullWidthNotification } from '../FullWidthNotification';
7
- import { MessageNotification } from '../MessageNotification';
8
7
  /**
9
8
  * React 텍스트 링크(`supportTextLink`)의 프로토콜 검사 (#538 리뷰 H2).
10
9
  * Vanilla `renderSupportingText` 와 같은 정책 — unsafe URL 이면 링크 없이 텍스트만 렌더한다.
@@ -27,12 +26,7 @@ function mount(element) {
27
26
  });
28
27
  return container;
29
28
  }
30
- const IMPLEMENTATIONS = [['MessageNotification(block)', supportTextLink => mount(/*#__PURE__*/createElement(MessageNotification, {
31
- title: '공지',
32
- variant: 'block',
33
- supportingText: '자세히 보기',
34
- supportTextLink
35
- })), '.ncua-message-notification__supporting-text'], ['FullWidthNotification', supportTextLink => mount(/*#__PURE__*/createElement(FullWidthNotification, {
29
+ const IMPLEMENTATIONS = [['FullWidthNotification', supportTextLink => mount(/*#__PURE__*/createElement(FullWidthNotification, {
36
30
  title: '공지',
37
31
  supportingText: '자세히 보기',
38
32
  supportTextLink