@yeongseoksong/framework 1.4.0 → 1.6.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.
package/README.md CHANGED
@@ -16,7 +16,9 @@ pnpm add @mantine/core @mantine/hooks @mantine/carousel @mantine/notifications @
16
16
 
17
17
  ## 설정
18
18
 
19
- ### 1. MantineProvider + theme
19
+ ### 1. SdProvider
20
+
21
+ `SdProvider` 하나가 **프로바이더/컨텍스트 조립**을 묶습니다 — `MantineProvider`(테마) + `SdToastProvider`(토스트 렌더 지점) + `NavProvider`(navItems Context). 이걸 최상단에 두고, 헤더/푸터 크롬은 `MainLayout`(또는 페이지별 `PageLayout`)으로 그 **안쪽**에 배치하세요.
20
22
 
21
23
  ```tsx
22
24
  // app/layout.tsx
@@ -24,24 +26,41 @@ import '@mantine/core/styles.css'
24
26
  import '@mantine/carousel/styles.css'
25
27
  import '@mantine/notifications/styles.css'
26
28
  import '@mantine/dates/styles.css'
27
- import { MantineProvider } from '@mantine/core'
28
- import { theme, SdToastProvider } from '@yeongseoksong/framework/ui'
29
+ import { ColorSchemeScript, mantineHtmlProps } from '@mantine/core'
30
+ import { SdProvider, MainLayout } from '@yeongseoksong/framework/ui'
31
+ import { navItems, companyInfo } from './data'
29
32
 
30
33
  export default function RootLayout({ children }: { children: React.ReactNode }) {
31
34
  return (
32
- <html lang="ko">
35
+ <html lang="ko" {...mantineHtmlProps}>
36
+ <head>
37
+ <ColorSchemeScript defaultColorScheme="light" />
38
+ </head>
33
39
  <body>
34
- <MantineProvider theme={theme}>
35
- <SdToastProvider />
36
- {children}
37
- </MantineProvider>
40
+ <SdProvider navItems={navItems}>
41
+ <MainLayout navItems={navItems} companyInfo={companyInfo}>
42
+ {children}
43
+ </MainLayout>
44
+ </SdProvider>
38
45
  </body>
39
46
  </html>
40
47
  )
41
48
  }
42
49
  ```
43
50
 
44
- `SdToastProvider`는 토스트가 실제로 그려지는 자리입니다. 넣지 않으면 `SdToast.*` 호출이 조용히 아무 일도 하지 않습니다(아래 SdToast 항목 참고).
51
+ **SdProvider props**
52
+
53
+ | prop | 기본값 | 설명 |
54
+ | -------------------- | ------------------ | -------------------------------------------------------------------- |
55
+ | `navItems` | (필수) | `NavProvider` Context에 얹을 네비게이션 트리 |
56
+ | `theme` | 프레임워크 기본 | 색 오버라이드는 `mergeThemeOverrides(theme, …)` 결과를 넘긴다(2번 참고) |
57
+ | `defaultColorScheme` | `light` | Mantine 색 구성 |
58
+
59
+ 헤더/푸터를 안 붙이는 화면(로그인 등)은 `MainLayout` 없이 `SdProvider` 안에 바로 내용을 둡니다. 헤더 변형(`mega`/`simple`/`panel`)·`loginFlag`·`companyInfo`는 이제 `MainLayout` prop입니다(아래 MainLayout 항목).
60
+
61
+ `ColorSchemeScript`와 Mantine CSS import는 `<head>`/서버 layout 몫이라 `SdProvider`에 포함되지 않습니다. 회사명/로고는 `SdProvider`가 아니라 env var로 주입합니다(2번 참고).
62
+
63
+ 내부의 `SdToastProvider`는 토스트가 실제로 그려지는 자리입니다 — `SdProvider`를 쓰면 자동으로 포함됩니다(아래 SdToast 항목 참고). `SdProvider`는 `navItems`를 `NavProvider`(React Context)로도 감싸므로, 그 아래의 `SdBreadcrumb`는 `navItems` prop 없이도 동작합니다.
45
64
 
46
65
  ### 2. 환경변수 설정
47
66
 
@@ -99,10 +118,9 @@ export const appTheme = mergeThemeOverrides(theme, {
99
118
 
100
119
  ```tsx
101
120
  // app/layout.tsx — Server Component 그대로 둡니다
102
- import { MantineProvider } from '@mantine/core'
103
121
  import { appTheme } from './theme'
104
122
 
105
- <MantineProvider theme={appTheme} defaultColorScheme="light">
123
+ <SdProvider theme={appTheme} navItems={navItems}>
106
124
  ```
107
125
 
108
126
  오버라이드하지 않은 키(타이포·spacing·shadows·컴포넌트 기본값)는 프레임워크 값이 그대로 유지됩니다.
@@ -125,7 +143,7 @@ import { appTheme } from './theme'
125
143
  | 경로 | 내용 |
126
144
  | -------------------------------- | --------------------------------------------- |
127
145
  | `@yeongseoksong/framework/ui` | UI 컴포넌트 전체 + `theme` (`"use client"`) |
128
- | `@yeongseoksong/framework/store` | Zustand 스토어 — `useAuthStore` · `useUiStore` · `useSdForm` (`"use client"`) |
146
+ | `@yeongseoksong/framework/store` | Zustand 스토어 — `useAuthStore` · `useUiStore` · `useNavStore` · `useSdForm` (`"use client"`) |
129
147
  | `@yeongseoksong/framework/util` | `t()`, 한글 조사(`josa` · `withJosa` · `fixJosa`), `runFinalizers`, `filterAndSort`, `COMPANY_NAME`, `LOGO_SRC`, `LOGO_ALT` |
130
148
  | `@yeongseoksong/framework/types` | 공유 인터페이스 |
131
149
 
@@ -170,11 +188,12 @@ import { SdTextBody } from '@yeongseoksong/framework/ui'
170
188
  | `SdSolutionCard` | `SdSolutionCardItem` `SdSolutionCardGrid` |
171
189
  | `SdClients` | `SdClientsGrid` `SdClientsMarquee` |
172
190
  | `SdMap` | `SdMapSingle` `SdMapTabs` |
191
+ | `SdBreadcrumb` | `SdBreadcrumb` (변형 없는 단일 컴포넌트 — 이름 자체가 flat export) |
173
192
  | `SdErrorView` | `SdErrorViewPage` `SdErrorViewNotFound` |
174
193
  | `SdLoginView` | `SdLoginViewCard` `SdLoginViewSplit` |
175
194
  | `SdResult` | `SdResultSuccess` `SdResultError` |
176
195
  | `SdToast` | `SdToastSuccess` `SdToastError` `SdToastWarning` `SdToastInfo` `SdToastLoading` `SdToastUpdate` `SdToastHide` `SdToastClean` |
177
- | `SdHeader` | `SdHeaderMega` `SdHeaderSimple` (`SdHeader` 자체는 `Mega`와 동일) |
196
+ | `SdHeader` | `SdHeaderMega` `SdHeaderSimple` `SdHeaderPanel` (`SdHeader` 자체는 `Mega`와 동일) |
178
197
 
179
198
  **클라이언트 컴포넌트에서는 네임스페이스 형태(`SdText.Body`)를 그대로 써도 됩니다.** `SdModal`은 `opened`/`onClose` 상태가 필요해 애초에 클라이언트 전용이므로 flat export가 없습니다.
180
199
 
@@ -435,6 +454,38 @@ const clients: ClientItem[] = [
435
454
  <SdClients.Marquee items={clients} speed={40} />
436
455
  ```
437
456
 
457
+ ### SdBreadcrumb
458
+
459
+ `SdHeader`와 **같은 `navItems`**를 넘기면, `parentId` 트리에서 현재 경로가 놓인 위치를 찾아
460
+ `홈 아이콘 > 조상 > … > 현재 페이지` 순의 브레드크럼을 그립니다. 마지막 크럼(현재 페이지)은
461
+ 링크가 아닌 강조 텍스트로 렌더됩니다.
462
+
463
+ ```tsx
464
+ import { SdBreadcrumb } from '@yeongseoksong/framework/ui'
465
+
466
+ // currentHref를 생략하면 usePathname()으로 현재 경로를 자동 추론합니다.
467
+ // 예: /about/company/history → 홈 > 소개 > 회사소개 > 연혁
468
+ <SdBreadcrumb navItems={navItems} />
469
+
470
+ // 명시적으로 경로를 줄 수도 있습니다(상세 페이지 등 정적으로 알고 있을 때).
471
+ <SdBreadcrumb navItems={navItems} currentHref="/about/company/history" />
472
+ ```
473
+
474
+ - **자동 추론** — `currentHref`가 없으면 `next/navigation`의 `usePathname()`을 씁니다. Next.js 앱
475
+ 라우터 컨텍스트 안에서 렌더해야 합니다.
476
+ - **접두 매칭** — 정확히 일치하는 `href`가 없으면, 현재 경로의 상위 경로인 `href` 중 가장 구체적인
477
+ 것을 현재로 삼습니다. 목록(`/blog`) 아래 상세(`/blog/123`) 페이지에서 목록까지의 트레일이 잡힙니다.
478
+ - **홈 크럼** — 기본 `/`로 링크됩니다. `homeHref`/`homeLabel`로 바꿀 수 있습니다.
479
+
480
+ > **`PageLayout`에 기본 내장** — `PageLayout`(Image/Minimal/Brand/Plain)에 `navItems`를 넘기면
481
+ > 본문 최상단에 이 브레드크럼이 **자동으로** 붙습니다. `breadcrumb={false}`로 끄고, `currentHref`로
482
+ > 경로를 강제할 수 있습니다.
483
+ >
484
+ > ```tsx
485
+ > <PageLayout.Minimal navItems={navItems} title="제조">…</PageLayout.Minimal> // 브레드크럼 자동
486
+ > <PageLayout.Minimal navItems={navItems} breadcrumb={false} title="제조">…</PageLayout.Minimal> // 끄기
487
+ > ```
488
+
438
489
  ### SdHeader / SdFooter
439
490
 
440
491
  ```tsx
@@ -491,6 +542,15 @@ const policyLinks: NavItem[] = [
491
542
  <SdHeader.Simple navItems={navItems} loginFlag />
492
543
  ```
493
544
 
545
+ 바 높이는 `Simple`처럼 60px로 고정하되 드롭다운 내부는 `Mega`와 같은 그룹 컬럼으로 펼치고 싶다면
546
+ `SdHeader.Panel`(서버 컴포넌트에서는 `SdHeaderPanel`)을 씁니다. 상위 항목마다 붙는 개별 `Menu`는 그대로지만,
547
+ 드롭다운 안을 `Menu.Item`/`Menu.Sub` 플라이아웃 대신 자식 링크 + 손자를 자식 아래 하위 링크로 얹은
548
+ 세로 패널로 채웁니다. 모바일 드로어는 다른 변형과 동일합니다.
549
+
550
+ ```tsx
551
+ <SdHeader.Panel navItems={navItems} loginFlag />
552
+ ```
553
+
494
554
  `navItems`는 같은 `parentId` 구조로 푸터 링크 컬럼도 만들고, 구분선 아래 하단 바에 카피라이트 · `policyLinks` · `company.socials` 아이콘이 놓입니다.
495
555
  `socials.platform`은 `x | youtube | instagram | facebook | linkedin | github | blog`를 지원합니다.
496
556
 
@@ -525,7 +585,7 @@ export default function LoginPage() {
525
585
  `Split`은 `brandTitle`/`brandDescription`으로 좌측 패널 문구를 받고(기본값 `%c`), 브랜드 면은 `PageLayout.Brand` 히어로와 같은 배경(`ui/surface.ts`)을 씁니다. 좌측 패널은 `md` 미만에서 숨겨져 폼만 남습니다.
526
586
  전체 화면이 기본(`mih="100svh"`)이므로, 좁은 영역에 끼워 넣을 때만 `mih`를 줄입니다.
527
587
 
528
- ### 상태 관리 — useAuthStore / useUiStore
588
+ ### 상태 관리 — useAuthStore / useUiStore / useNavStore
529
589
 
530
590
  전역 클라이언트 상태는 **Zustand** 스토어로 **`@yeongseoksong/framework/store`** 경로에서 제공됩니다. Provider가 없으므로 어디서든 훅으로 바로 읽고 씁니다.
531
591
 
@@ -570,6 +630,23 @@ const setGlobalLoading = useUiStore((s) => s.setGlobalLoading)
570
630
 
571
631
  `SdHeader`의 모바일 드로어는 일부러 이 스토어를 쓰지 않습니다 — 한 페이지에 헤더가 둘 이상 있으면 드로어가 함께 열리므로 인스턴스 로컬 상태로 남겨 두었습니다.
572
632
 
633
+ `useNavStore`는 네비게이션 상태(`navItems` + `setNavItems`)를 담습니다 — 렌더 트리 **밖**에서 navItems가 필요할 때(라우팅 로직 등) 씁니다.
634
+
635
+ ```tsx
636
+ import { useNavStore } from '@yeongseoksong/framework/store'
637
+ useNavStore.getState().setNavItems(navItems)
638
+ ```
639
+
640
+ 트리 **안** 컴포넌트(`SdBreadcrumb`)는 이 스토어가 아니라 `@yeongseoksong/framework/ui`의 `NavProvider`(React Context) + `useNav`로 navItems를 읽습니다 — `ui` 번들이 스토어를 직접 import하면 `dist/ui`에 인라인돼 인스턴스가 갈라지기 때문입니다. `SdProvider`가 `NavProvider`를 자동으로 감싸므로, 보통은 소비자가 직접 다룰 일이 없습니다.
641
+
642
+ ```tsx
643
+ import { NavProvider, useNav, SdBreadcrumb } from '@yeongseoksong/framework/ui'
644
+
645
+ <NavProvider navItems={navItems}>
646
+ <SdBreadcrumb /> {/* navItems prop 없이 useNav()로 읽음 */}
647
+ </NavProvider>
648
+ ```
649
+
573
650
  ### 폼 — useSdForm
574
651
 
575
652
  모든 폼이 같은 방식으로 값·검증·제출을 다루도록 훅 하나로 통일했습니다.
@@ -734,7 +811,7 @@ export default function Page() {
734
811
  }
735
812
  ```
736
813
 
737
- `headerVariant`로 어떤 헤더를 쓸지 고릅니다 — `mega`(기본, hover 시 확장되는 메가 메뉴) 또는 `simple`(바 높이 고정 + 항목별 드롭다운).
814
+ `headerVariant`로 어떤 헤더를 쓸지 고릅니다 — `mega`(기본, hover 시 확장되는 메가 메뉴), `simple`(바 높이 고정 + 항목별 드롭다운), `panel`(바 높이 고정 + 드롭다운 내부를 Mega식 그룹 컬럼으로).
738
815
 
739
816
  ```tsx
740
817
  <MainLayout navItems={navItems} companyInfo={company} headerVariant="simple" loginFlag>
@@ -75,6 +75,7 @@ __export(store_exports, {
75
75
  useAuthHydrated: () => useAuthHydrated,
76
76
  useAuthStore: () => useAuthStore,
77
77
  useFormStore: () => useFormStore,
78
+ useNavStore: () => useNavStore,
78
79
  useSdForm: () => useSdForm,
79
80
  useUiStore: () => useUiStore
80
81
  });
@@ -125,9 +126,16 @@ var useUiStore = (0, import_zustand2.create)()((set) => ({
125
126
  toggleSideNav: () => set((state) => ({ sideNavOpened: !state.sideNavOpened }))
126
127
  }));
127
128
 
129
+ // store/nav.store.ts
130
+ var import_zustand3 = require("zustand");
131
+ var useNavStore = (0, import_zustand3.create)()((set) => ({
132
+ navItems: [],
133
+ setNavItems: (navItems) => set({ navItems })
134
+ }));
135
+
128
136
  // store/form.state.ts
129
137
  var import_react2 = require("react");
130
- var import_zustand3 = require("zustand");
138
+ var import_zustand4 = require("zustand");
131
139
 
132
140
  // ui/atom/Toast.tsx
133
141
  var import_notifications = require("@mantine/notifications");
@@ -216,7 +224,7 @@ function patch(state, formId, next) {
216
224
  if (!current) return state;
217
225
  return __spreadProps(__spreadValues({}, state), { forms: __spreadProps(__spreadValues({}, state.forms), { [formId]: __spreadValues(__spreadValues({}, current), next) }) });
218
226
  }
219
- var useFormStore = (0, import_zustand3.create)()((set) => ({
227
+ var useFormStore = (0, import_zustand4.create)()((set) => ({
220
228
  forms: {},
221
229
  ensureForm: (formId, initialValues) => set(
222
230
  (state) => state.forms[formId] ? state : __spreadProps(__spreadValues({}, state), { forms: __spreadProps(__spreadValues({}, state.forms), { [formId]: blankEntry(initialValues) }) })
@@ -379,6 +387,7 @@ var formRules = {
379
387
  useAuthHydrated,
380
388
  useAuthStore,
381
389
  useFormStore,
390
+ useNavStore,
382
391
  useSdForm,
383
392
  useUiStore
384
393
  });
@@ -81,6 +81,36 @@ interface UiState {
81
81
  */
82
82
  declare const useUiStore: zustand.UseBoundStore<zustand.StoreApi<UiState>>;
83
83
 
84
+ interface CommonInfo {
85
+ id: number;
86
+ order: number;
87
+ isShow: boolean;
88
+ createdAt?: Date;
89
+ updatedAt?: Date;
90
+ }
91
+ interface NavItem extends CommonInfo {
92
+ label: string;
93
+ href?: string;
94
+ highlight?: boolean;
95
+ parentId?: number;
96
+ }
97
+
98
+ interface NavState {
99
+ /** 앱 전역 네비게이션 트리. 소비자가 앱 진입 시 `setNavItems`로 한 번 주입한다. */
100
+ navItems: NavItem[];
101
+ setNavItems: (navItems: NavItem[]) => void;
102
+ }
103
+ /**
104
+ * 네비게이션 상태 스토어 — 렌더 트리 **밖에서** navItems에 접근해야 할 때 쓴다
105
+ * (프로그램적 네비게이션, 라우팅 로직 등). 소비자가 `setNavItems`로 채운다.
106
+ *
107
+ * 트리 **안** 컴포넌트(`SdBreadcrumb`)는 이 스토어가 아니라 `ui/template/NavProvider`의
108
+ * React Context(`useNav`)에서 navItems를 읽는다 — `ui` 번들이 스토어를 import하면
109
+ * `dist/ui`에 인라인돼 `create()`가 두 번 도는 dual-bundle 문제가 생기기 때문이다
110
+ * (store/index.ts 상단 주석 참고). Context는 모듈 상태가 없어 그 문제에서 자유롭다.
111
+ */
112
+ declare const useNavStore: zustand.UseBoundStore<zustand.StoreApi<NavState>>;
113
+
84
114
  /**
85
115
  * 어떤 작업이 끝난 뒤 항상 돌려야 하는 뒷정리 — 라우팅, 모달 닫기, 목록 새로고침 등.
86
116
  *
@@ -204,4 +234,4 @@ declare const formRules: {
204
234
  sameAs: <V extends FormValues>(other: keyof V, message?: string) => FormRule<V>;
205
235
  };
206
236
 
207
- export { type AuthUser, type FormErrors, type FormRule, type FormValues, type SdFormApi, type SdFormOptions, formRules, useAuthHydrated, useAuthStore, useFormStore, useSdForm, useUiStore };
237
+ export { type AuthUser, type FormErrors, type FormRule, type FormValues, type SdFormApi, type SdFormOptions, formRules, useAuthHydrated, useAuthStore, useFormStore, useNavStore, useSdForm, useUiStore };
@@ -81,6 +81,36 @@ interface UiState {
81
81
  */
82
82
  declare const useUiStore: zustand.UseBoundStore<zustand.StoreApi<UiState>>;
83
83
 
84
+ interface CommonInfo {
85
+ id: number;
86
+ order: number;
87
+ isShow: boolean;
88
+ createdAt?: Date;
89
+ updatedAt?: Date;
90
+ }
91
+ interface NavItem extends CommonInfo {
92
+ label: string;
93
+ href?: string;
94
+ highlight?: boolean;
95
+ parentId?: number;
96
+ }
97
+
98
+ interface NavState {
99
+ /** 앱 전역 네비게이션 트리. 소비자가 앱 진입 시 `setNavItems`로 한 번 주입한다. */
100
+ navItems: NavItem[];
101
+ setNavItems: (navItems: NavItem[]) => void;
102
+ }
103
+ /**
104
+ * 네비게이션 상태 스토어 — 렌더 트리 **밖에서** navItems에 접근해야 할 때 쓴다
105
+ * (프로그램적 네비게이션, 라우팅 로직 등). 소비자가 `setNavItems`로 채운다.
106
+ *
107
+ * 트리 **안** 컴포넌트(`SdBreadcrumb`)는 이 스토어가 아니라 `ui/template/NavProvider`의
108
+ * React Context(`useNav`)에서 navItems를 읽는다 — `ui` 번들이 스토어를 import하면
109
+ * `dist/ui`에 인라인돼 `create()`가 두 번 도는 dual-bundle 문제가 생기기 때문이다
110
+ * (store/index.ts 상단 주석 참고). Context는 모듈 상태가 없어 그 문제에서 자유롭다.
111
+ */
112
+ declare const useNavStore: zustand.UseBoundStore<zustand.StoreApi<NavState>>;
113
+
84
114
  /**
85
115
  * 어떤 작업이 끝난 뒤 항상 돌려야 하는 뒷정리 — 라우팅, 모달 닫기, 목록 새로고침 등.
86
116
  *
@@ -204,4 +234,4 @@ declare const formRules: {
204
234
  sameAs: <V extends FormValues>(other: keyof V, message?: string) => FormRule<V>;
205
235
  };
206
236
 
207
- export { type AuthUser, type FormErrors, type FormRule, type FormValues, type SdFormApi, type SdFormOptions, formRules, useAuthHydrated, useAuthStore, useFormStore, useSdForm, useUiStore };
237
+ export { type AuthUser, type FormErrors, type FormRule, type FormValues, type SdFormApi, type SdFormOptions, formRules, useAuthHydrated, useAuthStore, useFormStore, useNavStore, useSdForm, useUiStore };
@@ -97,9 +97,16 @@ var useUiStore = create2()((set) => ({
97
97
  toggleSideNav: () => set((state) => ({ sideNavOpened: !state.sideNavOpened }))
98
98
  }));
99
99
 
100
+ // store/nav.store.ts
101
+ import { create as create3 } from "zustand";
102
+ var useNavStore = create3()((set) => ({
103
+ navItems: [],
104
+ setNavItems: (navItems) => set({ navItems })
105
+ }));
106
+
100
107
  // store/form.state.ts
101
108
  import { useCallback, useEffect as useEffect2, useMemo } from "react";
102
- import { create as create3 } from "zustand";
109
+ import { create as create4 } from "zustand";
103
110
 
104
111
  // ui/atom/Toast.tsx
105
112
  import { Notifications, notifications } from "@mantine/notifications";
@@ -188,7 +195,7 @@ function patch(state, formId, next) {
188
195
  if (!current) return state;
189
196
  return __spreadProps(__spreadValues({}, state), { forms: __spreadProps(__spreadValues({}, state.forms), { [formId]: __spreadValues(__spreadValues({}, current), next) }) });
190
197
  }
191
- var useFormStore = create3()((set) => ({
198
+ var useFormStore = create4()((set) => ({
192
199
  forms: {},
193
200
  ensureForm: (formId, initialValues) => set(
194
201
  (state) => state.forms[formId] ? state : __spreadProps(__spreadValues({}, state), { forms: __spreadProps(__spreadValues({}, state.forms), { [formId]: blankEntry(initialValues) }) })
@@ -350,6 +357,7 @@ export {
350
357
  useAuthHydrated,
351
358
  useAuthStore,
352
359
  useFormStore,
360
+ useNavStore,
353
361
  useSdForm,
354
362
  useUiStore
355
363
  };