@yeongseoksong/framework 1.5.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
 
@@ -567,7 +585,7 @@ export default function LoginPage() {
567
585
  `Split`은 `brandTitle`/`brandDescription`으로 좌측 패널 문구를 받고(기본값 `%c`), 브랜드 면은 `PageLayout.Brand` 히어로와 같은 배경(`ui/surface.ts`)을 씁니다. 좌측 패널은 `md` 미만에서 숨겨져 폼만 남습니다.
568
586
  전체 화면이 기본(`mih="100svh"`)이므로, 좁은 영역에 끼워 넣을 때만 `mih`를 줄입니다.
569
587
 
570
- ### 상태 관리 — useAuthStore / useUiStore
588
+ ### 상태 관리 — useAuthStore / useUiStore / useNavStore
571
589
 
572
590
  전역 클라이언트 상태는 **Zustand** 스토어로 **`@yeongseoksong/framework/store`** 경로에서 제공됩니다. Provider가 없으므로 어디서든 훅으로 바로 읽고 씁니다.
573
591
 
@@ -612,6 +630,23 @@ const setGlobalLoading = useUiStore((s) => s.setGlobalLoading)
612
630
 
613
631
  `SdHeader`의 모바일 드로어는 일부러 이 스토어를 쓰지 않습니다 — 한 페이지에 헤더가 둘 이상 있으면 드로어가 함께 열리므로 인스턴스 로컬 상태로 남겨 두었습니다.
614
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
+
615
650
  ### 폼 — useSdForm
616
651
 
617
652
  모든 폼이 같은 방식으로 값·검증·제출을 다루도록 훅 하나로 통일했습니다.
@@ -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
  };