@ihabdevteam/core 0.173.0 → 0.175.0

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.
@@ -543,7 +543,7 @@ export declare interface BoardProps<T> {
543
543
  }
544
544
 
545
545
  /** @public */
546
- export declare function BottomNav({ items, activeKey, onChange, appMode, onAppModeChange, moreItems, }: {
546
+ export declare function BottomNav({ items, activeKey, onChange, appMode, onAppModeChange, moreItems, scrollContainer, }: {
547
547
  items?: NavItem[];
548
548
  activeKey?: string;
549
549
  onChange?: (key: string) => void;
@@ -551,6 +551,13 @@ export declare function BottomNav({ items, activeKey, onChange, appMode, onAppMo
551
551
  onAppModeChange?: (mode: 'malgwi' | 'manager') => void;
552
552
  /** nav 항목과 별개로 항상 더보기 메뉴에 노출되는 유틸(알림·설정·계정 등) */
553
553
  moreItems?: BottomNavMoreItem[];
554
+ /**
555
+ * 갈래를 고를 때 **맨 위로 되돌릴 본문 스크롤 통** — CSS 고르개나 ref. `SideNav` 와 같은 뜻이다.
556
+ *
557
+ * 모바일에서도 갈래를 바꾸면 본문이 위에서 시작해야 한다 — 전에는 데스크톱(SideNav)만 되돌려, 같은 앱에서
558
+ * 큰 화면은 위로 가고 작은 화면은 앞 화면의 스크롤 자리에 남았다. 유틸(더보기 메뉴의 알림·설정 등)은 갈래가 아니라 되돌리지 않는다.
559
+ */
560
+ scrollContainer?: string | RefObject<HTMLElement | null>;
554
561
  }): JSX.Element;
555
562
 
556
563
  /**
@@ -1056,7 +1063,7 @@ export declare interface ChartPoint {
1056
1063
  * 공유하도록 두 컴포넌트가 내부적으로 사용한다.
1057
1064
  * @public
1058
1065
  */
1059
- export declare function ChartSection({ title, titleClassName, icon, disc, headRight, headSegment, headMenuItems, collapseBelow, onClick, ariaLabel, tooltip, subRow, className, inGrid, children }: ChartSectionProps): JSX.Element;
1066
+ export declare function ChartSection({ title, titleClassName, icon, disc, headRight, headSegment, headMenuItems, collapseBelow, onClick, ariaLabel, tooltip, totalNum, totalUnit, totalLabel, subRow, className, inGrid, children }: ChartSectionProps): JSX.Element;
1060
1067
 
1061
1068
  /** @public */
1062
1069
  export declare interface ChartSectionProps {
@@ -1084,6 +1091,13 @@ export declare interface ChartSectionProps {
1084
1091
  onClick?: () => void;
1085
1092
  /** 누르는 카드의 이름(낭독기) — 안 주면 제목이 글자일 때 그것을 쓴다 */
1086
1093
  ariaLabel?: string;
1094
+ /** 머리 오른쪽에 **한 줄로** 세우는 수(예: 정답률 83). 앞에 설명(`totalLabel`), 뒤에 큰 수와 단위(`totalUnit`).
1095
+ * `ChartTotal` 은 아래 줄(`subRow`)의 꼴이고, 이것은 **머리 줄** 의 꼴이다 — 소비자가 손으로 짜던 자리. */
1096
+ totalNum?: ReactNode;
1097
+ /** 수 뒤 단위(`%` 등) — `totalNum` 과 함께 쓴다 */
1098
+ totalUnit?: ReactNode;
1099
+ /** 수 앞 설명 한 줄 — 색을 입힌 대비 수치 등은 노드로 넘긴다(앱 규칙) */
1100
+ totalLabel?: ReactNode;
1087
1101
  /** 카드에 포인터가 닿을 때(키보드는 초점이 들어올 때) 뜨는 설명 — `StatChart tooltip` 과 같은 뜻.
1088
1102
  * 같은 줄에 선 StatChart 카드들만 설명이 뜨고 이 카드만 안 뜨던 것을 메운다. */
1089
1103
  tooltip?: string;
@@ -1556,7 +1570,7 @@ export declare function copyText(text: string): Promise<boolean>;
1556
1570
  *
1557
1571
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
1558
1572
  */
1559
- export declare const CORE_VERSION = "0.173.0";
1573
+ export declare const CORE_VERSION = "0.175.0";
1560
1574
 
1561
1575
  /** @public */
1562
1576
  declare const coreDefault: Record<string, unknown>;
@@ -2682,7 +2696,7 @@ export declare function hasStructureContrast(A: Decomp, B: Decomp): boolean;
2682
2696
  *
2683
2697
  * @public
2684
2698
  */
2685
- export declare function Header({ activeKey, onChange, items, malgwiItems: _malgwiItems, managerItems: _managerItems, unreadCount, onDebug, onNotification, onAccount, onSetting, onHelp, appMode, onAppModeChange }: {
2699
+ export declare function Header({ activeKey, onChange, items, malgwiItems: _malgwiItems, managerItems: _managerItems, unreadCount, onDebug, onNotification, onAccount, onSetting, onHelp, appMode, onAppModeChange, scrollContainer }: {
2686
2700
  activeKey?: string | null;
2687
2701
  onChange?: (key?: string) => void;
2688
2702
  /** 지금 모드의 내비 항목. 데스크톱은 SideNav, 모바일은 BottomNav 가 이 목록을 그린다. */
@@ -2702,6 +2716,13 @@ export declare function Header({ activeKey, onChange, items, malgwiItems: _malgw
2702
2716
  onHelp?: () => void;
2703
2717
  appMode?: 'malgwi' | 'manager';
2704
2718
  onAppModeChange?: (mode: 'malgwi' | 'manager') => void;
2719
+ /**
2720
+ * 갈래를 고를 때 **맨 위로 되돌릴 본문 스크롤 통** — CSS 고르개나 ref. 안쪽 `SideNav`·`BottomNav` 에 그대로 넘긴다.
2721
+ *
2722
+ * 본문이 `body` 가 아니라 제 통에서 구르는 셸(`body { overflow: hidden }`)이면 **반드시 준다** — 창 굴리기는 그 앱에서 아무 일도 하지 않는다.
2723
+ * **ref 로 주는 것이 가장 낫다**: 고르개 글자로 주면 코어가 소비자 클래스 이름을 다시 붙잡는 꼴이고, ref 면 그 얽힘이 아예 없다.
2724
+ */
2725
+ scrollContainer?: string | RefObject<HTMLElement | null>;
2705
2726
  }): JSX.Element;
2706
2727
 
2707
2728
  /** @public */
@@ -5926,6 +5947,7 @@ export declare type ShuffledItem = {
5926
5947
 
5927
5948
  declare type Side = '좌' | '우';
5928
5949
 
5950
+ /** @public */
5929
5951
  export declare function SideNav(props: SideNavProps): React_2.JSX.Element;
5930
5952
 
5931
5953
  /** @public */
@@ -1,3 +1,4 @@
1
+ import { type RefObject } from 'react';
1
2
  import './BottomNav.css';
2
3
  import './Tooltip/Tooltip.css';
3
4
  /** @public */
@@ -27,7 +28,7 @@ export type BottomNavMoreItem = {
27
28
  onClick?: () => void;
28
29
  };
29
30
  /** @public */
30
- export default function BottomNav({ items, activeKey, onChange, appMode, onAppModeChange, moreItems, }: {
31
+ export default function BottomNav({ items, activeKey, onChange, appMode, onAppModeChange, moreItems, scrollContainer, }: {
31
32
  items?: NavItem[];
32
33
  activeKey?: string;
33
34
  onChange?: (key: string) => void;
@@ -35,4 +36,11 @@ export default function BottomNav({ items, activeKey, onChange, appMode, onAppMo
35
36
  onAppModeChange?: (mode: 'malgwi' | 'manager') => void;
36
37
  /** nav 항목과 별개로 항상 더보기 메뉴에 노출되는 유틸(알림·설정·계정 등) */
37
38
  moreItems?: BottomNavMoreItem[];
39
+ /**
40
+ * 갈래를 고를 때 **맨 위로 되돌릴 본문 스크롤 통** — CSS 고르개나 ref. `SideNav` 와 같은 뜻이다.
41
+ *
42
+ * 모바일에서도 갈래를 바꾸면 본문이 위에서 시작해야 한다 — 전에는 데스크톱(SideNav)만 되돌려, 같은 앱에서
43
+ * 큰 화면은 위로 가고 작은 화면은 앞 화면의 스크롤 자리에 남았다. 유틸(더보기 메뉴의 알림·설정 등)은 갈래가 아니라 되돌리지 않는다.
44
+ */
45
+ scrollContainer?: string | RefObject<HTMLElement | null>;
38
46
  }): import("react").JSX.Element;
@@ -12,12 +12,13 @@ import MenuTooltip from './Tooltip/MenuTooltip';
12
12
  import './Tooltip/Tooltip.css';
13
13
  import { useAnchoredMenu, anchoredMenuZIndex } from '../hooks/useAnchoredMenu';
14
14
  import { useTranslation } from 'react-i18next';
15
+ import { scrollNavTargetToTop } from '../utils/navScroll';
15
16
  const BTN_SIZE = 52;
16
17
  const BTN_GAP = 10;
17
18
  /** 배지 숫자 — SideNav·MenuTooltip 과 같이 99 에서 자른다. 0 이하·없음은 0. */
18
19
  const badgeOf = (n) => (n && n > 0 ? n : 0);
19
20
  /** @public */
20
- export default function BottomNav({ items = [], activeKey, onChange, appMode, onAppModeChange, moreItems = [], }) {
21
+ export default function BottomNav({ items = [], activeKey, onChange, appMode, onAppModeChange, moreItems = [], scrollContainer, }) {
21
22
  const { t } = useTranslation();
22
23
  const nextMode = appMode === 'manager' ? 'malgwi' : 'manager';
23
24
  const modeIcon = appMode === 'manager' ? 'arrow-left' : 'arrow-right';
@@ -91,8 +92,10 @@ export default function BottomNav({ items = [], activeKey, onChange, appMode, on
91
92
  onClick: () => {
92
93
  if (it.state === 'disabled')
93
94
  return;
94
- if (it.key && onChange)
95
+ if (it.key && onChange) {
95
96
  onChange(it.key);
97
+ scrollNavTargetToTop(scrollContainer, 'BottomNav');
98
+ }
96
99
  handleCloseMore();
97
100
  },
98
101
  })),
@@ -115,6 +118,9 @@ export default function BottomNav({ items = [], activeKey, onChange, appMode, on
115
118
  const count = badgeOf(it.badgeCount);
116
119
  return (
117
120
  // key 는 선택 prop 이다 — 없으면 자리 번호로(형제 key 가 모두 undefined 가 되던 것)
118
- _jsxs("span", { className: "bottom-nav__slot", children: [_jsx(Button, { htmlType: "button", size: 52, type: "dot", icon: it.icon, ariaLabel: nameWithCount(it.label, count), selected: !!it.key && it.key === activeKey, disabled: it.state === 'disabled', className: "bottom-nav-item", onClick: () => it.key && onChange?.(it.key) }), count > 0 && (_jsx("span", { "aria-hidden": "true", children: _jsx(Badge, { type: "dot", position: "top-right", number: Math.min(count, 99), size: 24 }) }))] }, it.key ?? `__slot-${i}`));
121
+ _jsxs("span", { className: "bottom-nav__slot", children: [_jsx(Button, { htmlType: "button", size: 52, type: "dot", icon: it.icon, ariaLabel: nameWithCount(it.label, count), selected: !!it.key && it.key === activeKey, disabled: it.state === 'disabled', className: "bottom-nav-item", onClick: () => { if (it.key) {
122
+ onChange?.(it.key);
123
+ scrollNavTargetToTop(scrollContainer, 'BottomNav');
124
+ } } }), count > 0 && (_jsx("span", { "aria-hidden": "true", children: _jsx(Badge, { type: "dot", position: "top-right", number: Math.min(count, 99), size: 24 }) }))] }, it.key ?? `__slot-${i}`));
119
125
  }) }), showMore && (_jsxs(_Fragment, { children: [_jsx("div", { className: "bottom-nav__divider", "aria-hidden": "true" }), _jsx("div", { className: "bottom-nav__right", children: _jsxs("div", { ref: moreRef, className: "bottom-nav__slot", children: [_jsx(Button, { htmlType: "button", size: 52, type: "dot", icon: activeMore?.icon ?? 'ellipsis', ariaLabel: nameWithCount(activeMore?.label ?? t('nav.more', '더보기'), moreBadge), selected: !!activeMore, className: "bottom-nav-item", onClick: handleToggleMore, "aria-expanded": moreOpen, "aria-controls": moreOpen ? moreMenuId : undefined }), moreBadge > 0 && (_jsx("span", { "aria-hidden": "true", children: _jsx(Badge, { type: "dot", position: "top-right", number: Math.min(moreBadge, 99), size: 24 }) })), moreOpen && createPortal(_jsx("div", { id: moreMenuId, role: "group", "aria-label": t('nav.moreMenu', '더보기 메뉴'), className: "tooltip-content", style: menuStyle, ref: menuRef, children: _jsx(MenuTooltip, { columns: moreColumns, onClose: handleCloseMore }) }), document.body)] }) })] }))] }));
120
126
  }
@@ -65,6 +65,31 @@
65
65
  color: var(--color-gray);
66
66
  margin: 0;
67
67
  }
68
+ /* 머리 오른쪽의 수 — 앞에 설명, 뒤에 큰 수. **수는 줄을 안 넘긴다**(설명만 줄어든다) */
69
+ .manager-section__head-total {
70
+ display: flex;
71
+ align-items: baseline;
72
+ gap: var(--spacing-xs);
73
+ min-width: 0;
74
+ }
75
+ .manager-section__head-total-label {
76
+ color: var(--color-gray);
77
+ min-width: 0;
78
+ }
79
+ .manager-section__head-total-value {
80
+ display: flex;
81
+ align-items: baseline;
82
+ gap: 2px;
83
+ white-space: nowrap;
84
+ flex-shrink: 0;
85
+ }
86
+ .manager-section__head-total-num {
87
+ color: var(--color-black);
88
+ }
89
+ .manager-section__head-total-unit {
90
+ color: var(--color-gray);
91
+ }
92
+
68
93
  .manager-section__head-right {
69
94
  display: flex;
70
95
  align-items: center;
@@ -61,6 +61,13 @@ export interface ChartSectionProps {
61
61
  onClick?: () => void;
62
62
  /** 누르는 카드의 이름(낭독기) — 안 주면 제목이 글자일 때 그것을 쓴다 */
63
63
  ariaLabel?: string;
64
+ /** 머리 오른쪽에 **한 줄로** 세우는 수(예: 정답률 83). 앞에 설명(`totalLabel`), 뒤에 큰 수와 단위(`totalUnit`).
65
+ * `ChartTotal` 은 아래 줄(`subRow`)의 꼴이고, 이것은 **머리 줄** 의 꼴이다 — 소비자가 손으로 짜던 자리. */
66
+ totalNum?: ReactNode;
67
+ /** 수 뒤 단위(`%` 등) — `totalNum` 과 함께 쓴다 */
68
+ totalUnit?: ReactNode;
69
+ /** 수 앞 설명 한 줄 — 색을 입힌 대비 수치 등은 노드로 넘긴다(앱 규칙) */
70
+ totalLabel?: ReactNode;
64
71
  /** 카드에 포인터가 닿을 때(키보드는 초점이 들어올 때) 뜨는 설명 — `StatChart tooltip` 과 같은 뜻.
65
72
  * 같은 줄에 선 StatChart 카드들만 설명이 뜨고 이 카드만 안 뜨던 것을 메운다. */
66
73
  tooltip?: string;
@@ -83,4 +90,4 @@ export interface ChartSectionProps {
83
90
  * 공유하도록 두 컴포넌트가 내부적으로 사용한다.
84
91
  * @public
85
92
  */
86
- export default function ChartSection({ title, titleClassName, icon, disc, headRight, headSegment, headMenuItems, collapseBelow, onClick, ariaLabel, tooltip, subRow, className, inGrid, children }: ChartSectionProps): import("react").JSX.Element;
93
+ export default function ChartSection({ title, titleClassName, icon, disc, headRight, headSegment, headMenuItems, collapseBelow, onClick, ariaLabel, tooltip, totalNum, totalUnit, totalLabel, subRow, className, inGrid, children }: ChartSectionProps): import("react").JSX.Element;
@@ -27,7 +27,7 @@ export function ChartTotal({ label, children }) {
27
27
  * 공유하도록 두 컴포넌트가 내부적으로 사용한다.
28
28
  * @public
29
29
  */
30
- export default function ChartSection({ title, titleClassName = 'h3', icon, disc, headRight, headSegment, headMenuItems, collapseBelow = 420, onClick, ariaLabel, tooltip, subRow, className, inGrid = false, children }) {
30
+ export default function ChartSection({ title, titleClassName = 'h3', icon, disc, headRight, headSegment, headMenuItems, collapseBelow = 420, onClick, ariaLabel, tooltip, totalNum, totalUnit, totalLabel, subRow, className, inGrid = false, children }) {
31
31
  const { t } = useTranslation();
32
32
  const secRef = useRef(null);
33
33
  /* 설명 툴팁 — StatChart 와 같은 결(카드 사각형 기준, 포인터가 나가거나 초점이 빠지면 닫힘).
@@ -93,7 +93,10 @@ export default function ChartSection({ title, titleClassName = 'h3', icon, disc,
93
93
  }
94
94
  }, [onClick]);
95
95
  const 오른쪽 = typeof headRight === 'function' ? headRight({ collapsed: 접힘 }) : headRight;
96
- const 머리오른쪽 = (오른쪽 || headSegment) ? (_jsxs("div", { className: "manager-section__head-right", children: [오른쪽, headSegment && (접힘 ? (_jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "ellipsis", ariaLabel: headSegment.ariaLabel ?? t('common.more', '더보기'), showTooltipInfo: headSegment.ariaLabel ?? t('common.more', '더보기'), "aria-haspopup": "menu", "aria-expanded": !!메뉴자리, onClick: 메뉴열기 })) : (_jsx(SegmentedControl, { size: 32, ariaLabel: headSegment.ariaLabel, options: headSegment.options.map(o => ({ key: o.key, label: o.label, disabled: o.disabled })), value: headSegment.value, onChange: headSegment.onChange })))] })) : null;
96
+ /* 머리 오른쪽 차례 — **수 → 고르개 → 나머지(상세보기 단추가 맨 끝)**.
97
+ 전에는 `headRight` 를 먼저 그려 상세보기 단추가 고르개 **앞** 에 섰다(사용자 지시 2026-09-17: 단추가 오른쪽 끝). */
98
+ const 머리수 = (totalNum != null || totalLabel) ? (_jsxs("div", { className: "manager-section__head-total", children: [totalLabel && _jsx("span", { className: "manager-section__head-total-label caption-1", children: totalLabel }), totalNum != null && (_jsxs("span", { className: "manager-section__head-total-value", children: [_jsx("span", { className: "manager-section__head-total-num h4", children: totalNum }), totalUnit && _jsx("span", { className: "manager-section__head-total-unit h5", children: totalUnit })] }))] })) : null;
99
+ const 머리오른쪽 = (오른쪽 || headSegment || 머리수) ? (_jsxs("div", { className: "manager-section__head-right", children: [머리수, headSegment && (접힘 ? (_jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "ellipsis", ariaLabel: headSegment.ariaLabel ?? t('common.more', '더보기'), showTooltipInfo: headSegment.ariaLabel ?? t('common.more', '더보기'), "aria-haspopup": "menu", "aria-expanded": !!메뉴자리, onClick: 메뉴열기 })) : (_jsx(SegmentedControl, { size: 32, ariaLabel: headSegment.ariaLabel, options: headSegment.options.map(o => ({ key: o.key, label: o.label, disabled: o.disabled })), value: headSegment.value, onChange: headSegment.onChange }))), 오른쪽] })) : null;
97
100
  return (_jsxs("section", { ref: secRef, className: ['manager-section', inGrid && 'manager-section--grid', onClick && 'manager-section--clickable', className].filter(Boolean).join(' '), onClick: onClick ? 카드누름 : undefined, onKeyDown: onClick ? 카드키 : undefined, onPointerEnter: tooltip ? 설명보이기 : undefined, onPointerLeave: tooltip ? 설명닫기 : undefined, onFocus: tooltip ? 설명보이기 : undefined, onBlur: tooltip ? 설명닫기 : undefined, role: onClick ? 'button' : undefined, tabIndex: onClick ? 0 : undefined, "aria-label": onClick ? (ariaLabel ?? (typeof title === 'string' ? title : undefined)) : undefined, children: [tooltip && 설명자리 && (_jsx(TooltipInfo, { anchorRect: {
98
101
  left: 설명자리.left,
99
102
  top: 설명자리.top + 21,
@@ -1,3 +1,4 @@
1
+ import { type RefObject } from 'react';
1
2
  import './Tooltip/Tooltip.css';
2
3
  import { type SideNavItem } from './SideNav';
3
4
  import './Header.css';
@@ -18,7 +19,7 @@ export declare function BrandIdentity({ appMode }: {
18
19
  *
19
20
  * @public
20
21
  */
21
- export default function Header({ activeKey, onChange, items, malgwiItems: _malgwiItems, managerItems: _managerItems, unreadCount, onDebug, onNotification, onAccount, onSetting, onHelp, appMode, onAppModeChange }: {
22
+ export default function Header({ activeKey, onChange, items, malgwiItems: _malgwiItems, managerItems: _managerItems, unreadCount, onDebug, onNotification, onAccount, onSetting, onHelp, appMode, onAppModeChange, scrollContainer }: {
22
23
  activeKey?: string | null;
23
24
  onChange?: (key?: string) => void;
24
25
  /** 지금 모드의 내비 항목. 데스크톱은 SideNav, 모바일은 BottomNav 가 이 목록을 그린다. */
@@ -38,6 +39,13 @@ export default function Header({ activeKey, onChange, items, malgwiItems: _malgw
38
39
  onHelp?: () => void;
39
40
  appMode?: 'malgwi' | 'manager';
40
41
  onAppModeChange?: (mode: 'malgwi' | 'manager') => void;
42
+ /**
43
+ * 갈래를 고를 때 **맨 위로 되돌릴 본문 스크롤 통** — CSS 고르개나 ref. 안쪽 `SideNav`·`BottomNav` 에 그대로 넘긴다.
44
+ *
45
+ * 본문이 `body` 가 아니라 제 통에서 구르는 셸(`body { overflow: hidden }`)이면 **반드시 준다** — 창 굴리기는 그 앱에서 아무 일도 하지 않는다.
46
+ * **ref 로 주는 것이 가장 낫다**: 고르개 글자로 주면 코어가 소비자 클래스 이름을 다시 붙잡는 꼴이고, ref 면 그 얽힘이 아예 없다.
47
+ */
48
+ scrollContainer?: string | RefObject<HTMLElement | null>;
41
49
  }): import("react").JSX.Element;
42
50
  /**
43
51
  * `Header`의 alias입니다. 프로젝트 내 `Header`라는 이름이 이미 사용 중일 때 충돌 없이 임포트할 수 있습니다.
@@ -42,7 +42,7 @@ export function BrandIdentity({ appMode }) {
42
42
  *
43
43
  * @public
44
44
  */
45
- export default function Header({ activeKey, onChange, items, malgwiItems: _malgwiItems, managerItems: _managerItems, unreadCount, onDebug, onNotification, onAccount, onSetting, onHelp, appMode, onAppModeChange }) {
45
+ export default function Header({ activeKey, onChange, items, malgwiItems: _malgwiItems, managerItems: _managerItems, unreadCount, onDebug, onNotification, onAccount, onSetting, onHelp, appMode, onAppModeChange, scrollContainer }) {
46
46
  const { t } = useTranslation();
47
47
  const isMobile = useMediaQuery("(max-width: 767px)");
48
48
  /* **모바일 하단 내비에 SideNav 항목을 옮겨 적는다.** 예전에는 `SideNavItem` 을 `NavItem` 으로
@@ -118,7 +118,7 @@ export default function Header({ activeKey, onChange, items, malgwiItems: _malgw
118
118
  }], [bottomMoreItems, activeKey, closeUtilMenu]);
119
119
  return (_jsxs("aside", { className: `header${appMode === 'manager' ? ' header--manager' : ''}`, children: [_jsxs("div", { className: "header__bi", children: [_jsx(BrandIdentity, { appMode: appMode }), _jsxs("div", { className: "header__menu-button", ref: utilTriggerRef, children: [_jsx(Button, { htmlType: "button", size: 48, type: "dot", shape: "pill", variant: "default", icon: 'menu', ariaLabel: onNotification && unreadCount && unreadCount > 0
120
120
  ? t('nav.badgeLabel', { label: t('header.menuOpen', '메뉴 열기'), count: unreadCount, defaultValue: '{{label}}, {{count}}개' })
121
- : t('header.menuOpen', '메뉴 열기'), onClick: toggleUtilMenu, "aria-expanded": utilOpen, "aria-controls": utilOpen ? utilMenuId : undefined }), !!onNotification && !!unreadCount && unreadCount > 0 && (_jsx(Badge, { type: "dot", number: Math.min(unreadCount, 99), size: 24, "aria-hidden": true })), utilOpen && createPortal(_jsx("div", { id: utilMenuId, role: "group", "aria-label": t('header.utilMenu', '유틸리티 메뉴'), className: "tooltip-content", style: utilMenuStyle, ref: utilMenuRef, children: _jsx(MenuTooltip, { columns: utilColumns, onClose: closeUtilMenu }) }), document.body)] })] }), _jsx(SideNav, { items: items, activeKey: activeKey ?? undefined, onChange: onChange, unreadCount: unreadCount, onDebug: onDebug, onNotification: onNotification, onAccount: onAccount, onSetting: onSetting, onHelp: onHelp, appMode: appMode, onAppModeChange: onAppModeChange }), isMobile && _jsx(BottomNav, { items: bottomItems, activeKey: activeKey ?? undefined, onChange: handleBottomChange, appMode: appMode, onAppModeChange: onAppModeChange })] }));
121
+ : t('header.menuOpen', '메뉴 열기'), onClick: toggleUtilMenu, "aria-expanded": utilOpen, "aria-controls": utilOpen ? utilMenuId : undefined }), !!onNotification && !!unreadCount && unreadCount > 0 && (_jsx(Badge, { type: "dot", number: Math.min(unreadCount, 99), size: 24, "aria-hidden": true })), utilOpen && createPortal(_jsx("div", { id: utilMenuId, role: "group", "aria-label": t('header.utilMenu', '유틸리티 메뉴'), className: "tooltip-content", style: utilMenuStyle, ref: utilMenuRef, children: _jsx(MenuTooltip, { columns: utilColumns, onClose: closeUtilMenu }) }), document.body)] })] }), _jsx(SideNav, { scrollContainer: scrollContainer, items: items, activeKey: activeKey ?? undefined, onChange: onChange, unreadCount: unreadCount, onDebug: onDebug, onNotification: onNotification, onAccount: onAccount, onSetting: onSetting, onHelp: onHelp, appMode: appMode, onAppModeChange: onAppModeChange }), isMobile && _jsx(BottomNav, { items: bottomItems, activeKey: activeKey ?? undefined, onChange: handleBottomChange, scrollContainer: scrollContainer, appMode: appMode, onAppModeChange: onAppModeChange })] }));
122
122
  }
123
123
  /**
124
124
  * `Header`의 alias입니다. 프로젝트 내 `Header`라는 이름이 이미 사용 중일 때 충돌 없이 임포트할 수 있습니다.
@@ -36,4 +36,5 @@ export interface SideNavProps {
36
36
  */
37
37
  scrollContainer?: string | RefObject<HTMLElement | null>;
38
38
  }
39
+ /** @public */
39
40
  export default function SideNav(props: SideNavProps): React.JSX.Element;
@@ -7,7 +7,7 @@ import Badge from './Badge';
7
7
  import React, { useCallback } from 'react';
8
8
  import { useTranslation } from 'react-i18next';
9
9
  import { isSafeOpenHref } from '../utils/safeHref';
10
- import { devWarnOnce } from '../utils/devWarn';
10
+ import { scrollNavTargetToTop } from '../utils/navScroll';
11
11
  /** 단추 이름에 개수를 붙인다 — 뱃지는 aria-hidden 이라 개수는 이름으로만 전해진다(BottomNav 와 같은 문구). */
12
12
  function nameWithCount(t, label, count) {
13
13
  return count && count > 0 ? t('nav.badgeLabel', { label, count, defaultValue: '{{label}}, {{count}}개' }) : label;
@@ -56,30 +56,6 @@ function SideNavButton(props) {
56
56
  return (_jsxs("button", { type: "button", className: btnClass, onClick: handleClick, "aria-label": typeof text === 'string' ? nameWithCount(t, text, badgeCount) : undefined, "aria-current": active ? 'page' : undefined, style: style, "aria-disabled": isDisabled, children: [iconElement, _jsx("span", { className: `side-nav__label button-1 f`, style: labelStyle, children: text }), badgeCount > 0 && (_jsx(Badge, { type: "dot", position: "inline", number: Math.min(badgeCount, 99), size: 24, "aria-hidden": true })), state === 'loading' && (_jsx("span", { className: "side-nav__spinner", role: "img", "aria-label": t('loader.loading', '불러오는 중…') }))] }));
57
57
  }
58
58
  /** @public */
59
- /** 기본 고르개 — 소비자가 `scrollContainer` 를 안 줄 때 찾아 보는 이름(옛 동작 유지). @internal */
60
- const 기본통고르개 = '.content-area';
61
- /**
62
- * 갈래를 고른 뒤 본문을 맨 위로. 통을 못 찾으면 창을 굴리는데, 본문이 제 통에서 구르는 셸에서는
63
- * 그것이 **아무 일도 하지 않는다** — 그때는 조용히 죽는 대신 개발 모드에 한 번 알린다.
64
- */
65
- function 맨위로(scrollContainer) {
66
- try {
67
- const el = typeof scrollContainer === 'string'
68
- ? document.querySelector(scrollContainer)
69
- : scrollContainer?.current ?? document.querySelector(기본통고르개);
70
- if (el) {
71
- el.scrollTop = 0;
72
- return;
73
- }
74
- const 문서 = document.scrollingElement ?? document.documentElement;
75
- /* 창도 구를 수 없으면(본문이 다른 통에서 구른다) 이 기능은 조용히 죽는다 — 그 사실을 알린다 */
76
- if (문서 && 문서.scrollHeight <= 문서.clientHeight) {
77
- devWarnOnce('side-nav-scroll-container', `[SideNav] 본문을 맨 위로 되돌릴 통을 못 찾았다(기본 ${기본통고르개}) — 창도 구를 수 없어 아무 일도 하지 않는다. scrollContainer 로 통을 알려 준다.`);
78
- }
79
- window.scrollTo({ top: 0 });
80
- }
81
- catch (_) { }
82
- }
83
59
  export default function SideNav(props) {
84
60
  const { t } = useTranslation();
85
61
  const { items = [], activeKey, onChange, unreadCount, onDebug, onNotification, onAccount, onSetting, onHelp, appMode, onAppModeChange, scrollContainer } = props || {};
@@ -105,7 +81,7 @@ export default function SideNav(props) {
105
81
  return;
106
82
  }
107
83
  onChange?.(it.key);
108
- 맨위로(scrollContainer);
84
+ scrollNavTargetToTop(scrollContainer, 'SideNav');
109
85
  /* 키보드로 고르면 포커스가 레일 안에 남으므로 태블릿 레일도 펼친 채 둔다(:focus-within) —
110
86
  아래 강제 접힘은 마우스가 들어와야 풀리므로 키보드 사용자에게 걸면 레일이 닫힌 채 포커스만 남는다. */
111
87
  if (!fromPointer)
@@ -0,0 +1,11 @@
1
+ import type { RefObject } from 'react';
2
+ /** 소비자가 통을 안 알려 줄 때 찾아 보는 이름 — 옛 동작 유지(말귀 셸의 본문). @internal */
3
+ export declare const 기본통고르개 = ".content-area";
4
+ /**
5
+ * 갈래를 고른 뒤 본문을 맨 위로 — 내비(SideNav·BottomNav)가 함께 쓴다. @internal
6
+ *
7
+ * 통을 못 찾으면 창을 굴리는데, 본문이 제 통에서 구르는 셸(`body { overflow: hidden }`)에서는
8
+ * 그것이 **아무 일도 하지 않는다** — 그때는 조용히 죽는 대신 개발 모드에 한 번 알린다.
9
+ * 코어가 `.content-area`(소비자 클래스 이름)를 붙잡고 있던 자리라, 부르는 쪽이 통을 알려 주는 것이 정본이다.
10
+ */
11
+ export declare function scrollNavTargetToTop(scrollContainer?: string | RefObject<HTMLElement | null>, 어디서?: string): void;
@@ -0,0 +1,27 @@
1
+ import { devWarnOnce } from './devWarn';
2
+ /** 소비자가 통을 안 알려 줄 때 찾아 보는 이름 — 옛 동작 유지(말귀 셸의 본문). @internal */
3
+ export const 기본통고르개 = '.content-area';
4
+ /**
5
+ * 갈래를 고른 뒤 본문을 맨 위로 — 내비(SideNav·BottomNav)가 함께 쓴다. @internal
6
+ *
7
+ * 통을 못 찾으면 창을 굴리는데, 본문이 제 통에서 구르는 셸(`body { overflow: hidden }`)에서는
8
+ * 그것이 **아무 일도 하지 않는다** — 그때는 조용히 죽는 대신 개발 모드에 한 번 알린다.
9
+ * 코어가 `.content-area`(소비자 클래스 이름)를 붙잡고 있던 자리라, 부르는 쪽이 통을 알려 주는 것이 정본이다.
10
+ */
11
+ export function scrollNavTargetToTop(scrollContainer, 어디서 = 'Nav') {
12
+ try {
13
+ const el = typeof scrollContainer === 'string'
14
+ ? document.querySelector(scrollContainer)
15
+ : scrollContainer?.current ?? document.querySelector(기본통고르개);
16
+ if (el) {
17
+ el.scrollTop = 0;
18
+ return;
19
+ }
20
+ const 문서 = document.scrollingElement ?? document.documentElement;
21
+ if (문서 && 문서.scrollHeight <= 문서.clientHeight) {
22
+ devWarnOnce('nav-scroll-container', `[${어디서}] 본문을 맨 위로 되돌릴 통을 못 찾았다(기본 ${기본통고르개}) — 창도 구를 수 없어 아무 일도 하지 않는다. scrollContainer 로 통을 알려 준다.`);
23
+ }
24
+ window.scrollTo({ top: 0 });
25
+ }
26
+ catch (_) { }
27
+ }
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.173.0";
49
+ export declare const CORE_VERSION = "0.175.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.173.0';
49
+ export const CORE_VERSION = '0.175.0';