@thefreshop/tb 1.1.2 → 1.1.4

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 ADDED
@@ -0,0 +1,273 @@
1
+ # @thefreshop/tb — tb framework
2
+
3
+ ## 1) 패키지 이름 + 한 줄 소개
4
+ Ant Design 기반의 React UI 프레임워크로, 탭형 어드민 프레임과 CRUD 테이블/검색/페이지 스캐폴드를 제공합니다.
5
+
6
+ ## 2) Why / What it solves
7
+ - 탭 기반 어드민 프레임(Top/Nav/Bottom)과 라우팅을 빠르게 구성.
8
+ - 검색/테이블/모달 CRUD 흐름을 재사용 가능한 컴포넌트로 표준화.
9
+ - Ant Design을 기반으로 일관된 관리자 UI 구축.
10
+
11
+ ## 3) Features
12
+ - [x] 탭 기반 프레임 레이아웃 + 동적 라우팅
13
+ - [x] 세션 기반 탭 상태 저장(`sessionStorage`)
14
+ - [x] 검색 박스 + 테이블 + 로딩 레이아웃 스캐폴드
15
+ - [x] Ant Design Table 기반 CRUD 래퍼 + 모달 생성/수정
16
+ - [x] 한국어 로케일 기본 제공(dayjs/moment/antd)
17
+
18
+ ## 4) Installation
19
+ ```bash
20
+ npm i @thefreshop/tb
21
+ ```
22
+
23
+ ```bash
24
+ yarn add @thefreshop/tb
25
+ ```
26
+
27
+ ```bash
28
+ pnpm add @thefreshop/tb
29
+ ```
30
+
31
+ ### Peer Dependencies
32
+ 이 패키지는 아래 피어 의존성을 필요로 합니다:
33
+ ```bash
34
+ npm i react react-dom react-router antd @ant-design/icons dayjs moment react-error-boundary @monaco-editor/react
35
+ ```
36
+
37
+ ## 5) Quick Start
38
+ ```tsx
39
+ import React from "react";
40
+ import { TbProvider, TbpProvider, TbFrame } from "@thefreshop/tb";
41
+ import type { navType, topType, tbframeType } from "@thefreshop/tb";
42
+
43
+ const setting: tbframeType = { islogin: false, startTabKey: "dashboard" };
44
+ const top: topType = { title: "TB Admin", topMenuSetting: [{ key: "main", title: "Main" }] };
45
+ const nav: navType = {
46
+ menuSet: [
47
+ {
48
+ parentkey: "main",
49
+ menuSetting: [
50
+ { tab: { key: "dashboard", title: "Dashboard", page: <div>Home</div> }, hasPage: true },
51
+ ],
52
+ },
53
+ ],
54
+ };
55
+
56
+ export default function App() {
57
+ return (
58
+ <TbProvider basename="/">
59
+ <TbpProvider>
60
+ <TbFrame setting={setting} top={top} nav={nav} />
61
+ </TbpProvider>
62
+ </TbProvider>
63
+ );
64
+ }
65
+ ```
66
+
67
+ ## 6) Usage
68
+ ### 기본 사용
69
+ ```tsx
70
+ import { Tbpage } from "@thefreshop/tb";
71
+
72
+ const columns = [
73
+ {
74
+ tableProps: { dataIndex: "name", title: "Name" },
75
+ search: true,
76
+ create: true,
77
+ modify: true,
78
+ },
79
+ ];
80
+
81
+ export default function Users() {
82
+ return (
83
+ <Tbpage
84
+ title="Users"
85
+ decs="User list"
86
+ searchOption={[{ key: "base", items: [{ type: "input", key: "name", title: "Name" }] }]}
87
+ tableProps={{ rowKey: "id", columns, dataSource: [] }}
88
+ />
89
+ );
90
+ }
91
+ ```
92
+
93
+ ### 옵션/설정(핵심 props 요약)
94
+ #### TbFrame
95
+ | 이름 | 타입 | 기본값 | 설명 |
96
+ | --- | --- | --- | --- |
97
+ | `setting` | `tbframeType` | - | 로그인/시작 탭 등 기본 프레임 설정 |
98
+ | `top` | `topType` | - | 상단 영역 설정 |
99
+ | `nav` | `navType` | - | 네비게이션/탭 정의 |
100
+ | `bottom` | `bottomType` | `undefined` | 하단 영역 |
101
+ | `top_banner` | `React.ReactNode` | `undefined` | 상단 배너 영역 |
102
+ | `hashmode` | `boolean` | `false` | 해시 라우팅 사용 여부 |
103
+ | `frameStyle` | `FrameStyleType` | `undefined` | 스타일 오버라이드 |
104
+
105
+ #### Tbpage
106
+ | 이름 | 타입 | 기본값 | 설명 |
107
+ | --- | --- | --- | --- |
108
+ | `tableProps` | `any` | - | AntBaseTable에 전달되는 props |
109
+ | `searchOption` | `any` | `undefined` | SearchBox 옵션 |
110
+ | `searchDisabled` | `boolean` | `false` | 검색 비활성화 |
111
+ | `onSubmit` | `(searchdata?, rowdata?, isReset?) => void` | `undefined` | 검색 제출/리셋 콜백 |
112
+ | `isLoading` | `boolean` | `undefined` | 로딩 오버레이 |
113
+ | `title`/`decs` | `string` | `undefined` | 타이틀/설명 |
114
+ | `PageStyle` | `PageStyleType` | `undefined` | 스타일 오버라이드 |
115
+
116
+ > 더 많은 props 및 타입은 `docs/API.md`를 참고하세요.
117
+
118
+ ### 반환값/에러
119
+ | 항목 | 반환 | 에러/주의사항 |
120
+ | --- | --- | --- |
121
+ | `useTbState()` | `ProviderType` | `TbProvider` 밖에서 호출 시 Error throw |
122
+
123
+ ## 7) API Reference
124
+ API가 많은 관계로 `docs/API.md`로 분리했습니다.
125
+ - 핵심 export: `TbProvider`, `useTbState`, `tbContext`, `TbFrame`, `TbpProvider`, `ContentLayout`, `Tbpage`, `TbpageSm`, `Tblist`, `TbpageMulti`, `AntBaseTable`, `AntBaseModalCreate`, `AuthTable` 및 타입들
126
+
127
+ ## 8) TypeScript
128
+ - 타입 엔트리: `package.json`의 `types: ./dist/cjs/index.d.ts`
129
+ - 공개 타입: `export type * from "./types"`로 노출
130
+
131
+ ```ts
132
+ import type { tbframeType, navType, antBasePropsType } from "@thefreshop/tb";
133
+ ```
134
+
135
+ ## 9) Compatibility
136
+ - **Runtime**: 브라우저 기반 React 앱 (DOM lib 사용)
137
+ - **React**: `react@^19.1.1` (peer)
138
+ - **Router**: `react-router@^7.10.0` (peer)
139
+ - **UI**: `antd@^6.0.0` (peer)
140
+ - **CJS/ESM**: `main`(CJS), `module`(ESM) 빌드 제공
141
+ - **Node 엔진**: Not found in repository
142
+
143
+ ## 10) Bundling / Tree-shaking
144
+ - `exports`/`sideEffects` 필드가 없어 트리 셰이킹 동작은 번들러 설정에 의존합니다.
145
+ - Rollup 빌드에서 CSS Modules를 런타임 inject 하므로 일부 부수효과(side effects)가 있을 수 있습니다.
146
+
147
+ ## 11) Examples
148
+ ### Basic (프레임 구성)
149
+ ```tsx
150
+ import { TbProvider, TbpProvider, TbFrame } from "@thefreshop/tb";
151
+ import type { navType, topType, tbframeType } from "@thefreshop/tb";
152
+
153
+ const setting: tbframeType = { islogin: false };
154
+ const top: topType = { title: "Admin", topMenuSetting: [{ key: "main", title: "Main" }] };
155
+ const nav: navType = {
156
+ menuSet: [
157
+ {
158
+ parentkey: "main",
159
+ menuSetting: [{ tab: { key: "home", title: "Home", page: <div>Home</div> }, hasPage: true }],
160
+ },
161
+ ],
162
+ };
163
+
164
+ export default function App() {
165
+ return (
166
+ <TbProvider>
167
+ <TbpProvider>
168
+ <TbFrame setting={setting} top={top} nav={nav} />
169
+ </TbpProvider>
170
+ </TbProvider>
171
+ );
172
+ }
173
+ ```
174
+
175
+ ### Advanced (검색 + 테이블)
176
+ ```tsx
177
+ import { Tbpage } from "@thefreshop/tb";
178
+
179
+ const columns = [
180
+ { tableProps: { dataIndex: "name", title: "Name" }, search: true, create: true, modify: true },
181
+ ];
182
+
183
+ export default function Users() {
184
+ return (
185
+ <Tbpage
186
+ title="Users"
187
+ searchOption={[{ key: "base", items: [{ type: "input", key: "name", title: "Name" }] }]}
188
+ tableProps={{ rowKey: "id", columns, dataSource: [] }}
189
+ onSubmit={(query, data) => {
190
+ console.log(query, data);
191
+ }}
192
+ />
193
+ );
194
+ }
195
+ ```
196
+
197
+ ### Real-world (멀티 패널)
198
+ ```tsx
199
+ import { TbpageMulti } from "@thefreshop/tb";
200
+
201
+ export default function Dashboard() {
202
+ return (
203
+ <TbpageMulti
204
+ title="Dashboard"
205
+ arrtableProps={[
206
+ { type: "table", title: "Left", tableprops: { rowKey: "id", columns: [], dataSource: [] } },
207
+ { type: "module", title: "Right", module: <div>Custom Module</div> },
208
+ ]}
209
+ arrflex={[2, 1]}
210
+ direction="row"
211
+ />
212
+ );
213
+ }
214
+ ```
215
+
216
+ ## 12) Troubleshooting
217
+ 1. **peerDependencies 충돌**: React/antd/react-router 버전이 맞지 않으면 런타임 오류가 발생합니다.
218
+ 2. **CSS Modules 미적용**: 소비 앱 번들러에서 CSS Modules가 비활성화되면 레이아웃이 깨집니다.
219
+ 3. **SSR 환경 오류**: `sessionStorage` 사용으로 SSR 시 `window` 관련 에러가 날 수 있습니다.
220
+ 4. **CJS/ESM 혼용 오류**: 번들러가 `module`/`main` 선택을 잘못하면 중복 React 경고가 발생할 수 있습니다.
221
+ 5. **타입 인식 실패**: `types` 경로(`dist/cjs/index.d.ts`)가 올바르게 배포되었는지 확인하세요.
222
+ 6. **Ant Design 스타일 누락**: 소비 앱에서 antd 스타일을 로드하지 않으면 컴포넌트가 깨집니다.
223
+ 7. **모달 폼 초기값 미반영**: `AntBaseModalCreate`에서 `initialValues` 사용 시 `open` 타이밍을 확인하세요.
224
+
225
+ ## 13) FAQ
226
+ 1. **React 18에서도 동작하나요?** Not found in repository (peerDependencies는 React 19 기준입니다).
227
+ 2. **React Router v6 사용 가능?** Not found in repository (peerDependencies는 v7 기준입니다).
228
+ 3. **라우팅 구조는 어떻게 만들나요?** `nav.menuSet`과 `nav.globalTabs` 기반으로 라우트가 자동 생성됩니다.
229
+ 4. **해시 라우팅을 지원하나요?** `TbFrame`의 `hashmode`로 지원합니다.
230
+ 5. **탭 상태는 어디에 저장되나요?** `sessionStorage`의 `tbFrameState` 키에 저장됩니다.
231
+ 6. **로케일 설정은 어디서 하나요?** `TbpProvider`가 `antd` `ConfigProvider`와 `ko_KR` 로케일을 설정합니다.
232
+ 7. **테스트 예제나 Storybook이 있나요?** Not found in repository.
233
+ 8. **변경 이력(Changelog)이 있나요?** No changelog found.
234
+
235
+ ## 14) Contributing
236
+ ### 로컬 개발
237
+ ```bash
238
+ yarn install
239
+ ```
240
+
241
+ ```bash
242
+ yarn dev
243
+ ```
244
+
245
+ ```bash
246
+ yarn build
247
+ ```
248
+
249
+ - 테스트/린트 스크립트: Not found in repository
250
+ - 배포 스크립트: Not found in repository
251
+
252
+ ## 15) Security
253
+ - 공급망 보안: peerDependencies 버전을 고정/검증하세요.
254
+ - 토큰/시크릿 관리: 레포에 환경변수 파일이 없으므로 소비 앱에서 안전하게 관리하세요.
255
+ - 취약점 신고: `package.json`의 `bugs.url` 참고.
256
+
257
+ ## 16) License
258
+ - `package.json`: MIT
259
+ - LICENSE 파일: No license file found
260
+
261
+ ## 17) Quick Commands
262
+ | 목적 | 명령 | 비고 |
263
+ | --- | --- | --- |
264
+ | 개발(와치) | ```bash
265
+ yarn dev
266
+ ``` | Rollup watch 빌드 |
267
+ | 빌드 | ```bash
268
+ yarn build
269
+ ``` | `dist/` 출력 |
270
+ | 테스트 | Not found in repository | - |
271
+ | 린트 | Not found in repository | - |
272
+ | 타입체크 | Not found in repository | - |
273
+ | 퍼블리시 | Not found in repository | - |
package/dist/cjs/index.js CHANGED
@@ -40,7 +40,7 @@ var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
40
40
  const tbContext = React.createContext(undefined);
41
41
 
42
42
  const SESSION_STORAGE_KEY = "tbFrameState";
43
- const TbProvider = ({ children }) => {
43
+ const TbProvider = ({ basename, children }) => {
44
44
  const [topkey, setTopkey] = React.useState("");
45
45
  const [tabList, setTabList] = React.useState([]);
46
46
  const [currentTabKey, setCurrentTabKey] = React.useState();
@@ -142,6 +142,7 @@ const TbProvider = ({ children }) => {
142
142
  errMsg,
143
143
  tabList,
144
144
  setTabList,
145
+ basename,
145
146
  } }, children));
146
147
  };
147
148
  const useTbState = () => {
@@ -402,11 +403,11 @@ var css_248z$7 = ".main-module_mainFrame__Etjvl{height:100%;width:100%}.main-mod
402
403
  var styles$5 = {"mainFrame":"main-module_mainFrame__Etjvl","mainFrameFullscreen":"main-module_mainFrameFullscreen__oEwV8"};
403
404
  styleInject(css_248z$7);
404
405
 
405
- const Main = () => {
406
+ const Main = ({ style }) => {
406
407
  const [sp] = reactRouter.useSearchParams();
407
408
  const isFs = sp.get("fs") === "1" || sp.get("fullscreen") === "1";
408
409
  // console.log("Main render, isFs:", isFs);
409
- return (React.createElement("div", { className: isFs ? styles$5.mainFrameFullscreen : styles$5.mainFrame },
410
+ return (React.createElement("div", { className: isFs ? styles$5.mainFrameFullscreen : styles$5.mainFrame, style: style },
410
411
  React.createElement(reactRouter.Outlet, null)));
411
412
  };
412
413
 
@@ -501,7 +502,7 @@ const NullPage = () => {
501
502
  };
502
503
 
503
504
  const TbFrame = ({ setting, top, bottom, nav, top_banner, hashmode = false, frameStyle, }) => {
504
- const { user, errMsg, tabList, setTabList, startTabKey, setStartTabKey, loginUser } = useTbState();
505
+ const { basename, user, errMsg, tabList, setTabList, startTabKey, setStartTabKey, loginUser } = useTbState();
505
506
  React.useEffect(() => {
506
507
  let p_tabList = [];
507
508
  nav.menuSet.forEach((item) => {
@@ -574,7 +575,9 @@ const TbFrame = ({ setting, top, bottom, nav, top_banner, hashmode = false, fram
574
575
  children: route(),
575
576
  errorElement: React.createElement(NullPage, null),
576
577
  },
577
- ]);
578
+ ], {
579
+ basename: basename ?? "/",
580
+ });
578
581
  }
579
582
  else {
580
583
  return reactRouter.createBrowserRouter([
@@ -584,9 +587,11 @@ const TbFrame = ({ setting, top, bottom, nav, top_banner, hashmode = false, fram
584
587
  children: route(),
585
588
  errorElement: React.createElement(NullPage, null),
586
589
  },
587
- ]);
590
+ ], {
591
+ basename: basename ?? "/",
592
+ });
588
593
  }
589
- }, [hashmode, user, top, nav, setting, top_banner, bottom, frameStyle]);
594
+ }, [hashmode, user, top, nav, setting, top_banner, bottom, frameStyle, basename]);
590
595
  return React.createElement(reactRouter.RouterProvider, { router: router });
591
596
  };
592
597
  const MainLayout = ({ errMsg, setting, top_banner, nav, user, top, bottom, frameStyle, }) => {
@@ -631,7 +636,7 @@ const MainLayout = ({ errMsg, setting, top_banner, nav, user, top, bottom, frame
631
636
  React.createElement(Nav, { nav: nav, islogin: setting.islogin ?? false, frameStyle: frameStyle }),
632
637
  React.createElement("div", { className: styles$9.contentsFrame, style: frameStyle?.contentsFrame },
633
638
  React.createElement(Tabs, { nav: nav, frameStyle: frameStyle }),
634
- React.createElement(Main, null))),
639
+ React.createElement(Main, { style: frameStyle?.contentsMainFrame }))),
635
640
  React.createElement("div", { className: styles$9.bottomFrame, style: frameStyle?.bottomFrame }, bottom && React.createElement(Bottom, { bottom: bottom, frameStyle: frameStyle }))))));
636
641
  };
637
642
 
@@ -700,7 +705,7 @@ const FORM_CREATE = "등록";
700
705
 
701
706
  * @todo
702
707
  */
703
- const TableTopButtonGroup = ({ isSelect, onRefresh, isCounter, autoRefreshTime, addProps, modifyProps, deleteProps, removeProps, resetProps, customProps, }) => {
708
+ const TableTopButtonGroup = ({ isSelect, onRefresh, isCounter, autoRefreshTime, addProps, modifyProps, deleteProps, removeProps, resetProps, customProps, isInSearch = false, onInSearch, }) => {
704
709
  if (!addProps && !modifyProps && !deleteProps && !removeProps && !resetProps && !customProps && !onRefresh) {
705
710
  return null;
706
711
  }
@@ -747,7 +752,8 @@ const TableTopButtonGroup = ({ isSelect, onRefresh, isCounter, autoRefreshTime,
747
752
  deleteProps ? (React.createElement(antd.Button, { danger: true, disabled: !isSelect || deleteProps.disable, onClick: deleteProps.onFun }, deleteProps.title ? deleteProps.title : TABLE_DELETE)) : null,
748
753
  resetProps ? (React.createElement(antd.Button, { disabled: !isSelect || resetProps.disable, onClick: resetProps.onFun }, resetProps.title ? resetProps.title : TABLE_RESET)) : null,
749
754
  removeProps ? (React.createElement(antd.Button, { danger: true, type: "primary", disabled: !isSelect || removeProps.disable, onClick: removeProps.onFun }, removeProps.title ? removeProps.title : TABLE_REMOVE)) : null,
750
- customProps?.rightRender && customProps?.rightRender()))));
755
+ customProps?.rightRender && customProps?.rightRender(),
756
+ isInSearch && onInSearch ? React.createElement(InSearchForm, { onInSearch: onInSearch }) : null))));
751
757
  };
752
758
  const TableBottomButtonGroup = ({ isSelect, topProps, bottomProps, upProps, downProps, }) => {
753
759
  return (React.createElement(antd.Row, { justify: "space-between" },
@@ -761,6 +767,22 @@ const TableBottomButtonGroup = ({ isSelect, topProps, bottomProps, upProps, down
761
767
  downProps ? (React.createElement(antd.Button, { disabled: !isSelect || downProps.disable, onClick: downProps.onFun }, TABLE_DOWN)) : null,
762
768
  bottomProps ? (React.createElement(antd.Button, { disabled: !isSelect || bottomProps.disable, onClick: bottomProps.onFun }, TABLE_BOTTOM)) : null))));
763
769
  };
770
+ const InSearchForm = ({ onInSearch }) => {
771
+ const [inputSearch, setInputSearch] = React.useState("");
772
+ const SearchClick = () => {
773
+ onInSearch(inputSearch);
774
+ };
775
+ const activeEnter = (e) => {
776
+ if (e.key === "Enter") {
777
+ SearchClick();
778
+ }
779
+ };
780
+ return (React.createElement(antd.Space.Compact, null,
781
+ React.createElement(antd.Input, { style: { width: "150px" }, onChange: (e) => {
782
+ setInputSearch(e.target.value);
783
+ }, value: inputSearch, onKeyDown: (e) => activeEnter(e) }),
784
+ React.createElement(antd.Button, { onClick: SearchClick }, "\uAC80\uC0C9")));
785
+ };
764
786
 
765
787
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
766
788
 
@@ -23338,11 +23360,24 @@ const AntBaseModalCreate = ({ modalProps, formProps, props }) => {
23338
23360
 
23339
23361
  const { Title: Title$1 } = antd.Typography;
23340
23362
  const { confirm: confirm$1 } = antd.Modal;
23341
- const AntBaseTable = ({ bordered, className, rowKey, rowName, dataSource, initData, customColumn, customValue, columns, expandable, size, onDoubleClick, onClick, selectProps, addProps, modifyProps, deleteProps, removeProps, resetProps, customProps, topProps, bottomProps, upProps, downProps, pageProps, scroll, onRefresh, isCounter, autoRefreshTime, createModalWidth = 1000, onCreateClose, }, ref) => {
23363
+ const AntBaseTable = ({ bordered, className, rowKey, rowName, dataSource, initData, customColumn, customValue, columns, expandable, size, onDoubleClick, onClick, selectProps, addProps, modifyProps, deleteProps, removeProps, resetProps, customProps, topProps, bottomProps, upProps, downProps, pageProps, scroll, onRefresh, isCounter, autoRefreshTime, createModalWidth = 1000, onCreateClose, isInSearch = false, }, ref) => {
23342
23364
  const [selectedRowKeys, setSelectedRowKeys] = React.useState([]);
23343
23365
  const [selectedRow, setSelectedRow] = React.useState();
23344
23366
  const [popupCreate, setPopupCreate] = React.useState(false);
23345
23367
  const [popupModify, setPopupModify] = React.useState(false);
23368
+ const [viewData, setViewData] = React.useState(dataSource);
23369
+ const onInSearch = (value) => {
23370
+ const search = value.trim().toLowerCase();
23371
+ if (!search) {
23372
+ setViewData(dataSource);
23373
+ return;
23374
+ }
23375
+ setViewData(dataSource.filter((item) => Object.values(item).some((itemValue) => {
23376
+ if (itemValue == null)
23377
+ return false;
23378
+ return String(itemValue).toLowerCase().includes(search);
23379
+ })));
23380
+ };
23346
23381
  React.useEffect(() => {
23347
23382
  setSelectedRow(dataSource?.find((m) => {
23348
23383
  if (m) {
@@ -23435,7 +23470,7 @@ const AntBaseTable = ({ bordered, className, rowKey, rowName, dataSource, initDa
23435
23470
  height: "100%",
23436
23471
  padding: "20px",
23437
23472
  } },
23438
- React.createElement(TableTopButtonGroup, { isSelect: isSelect, isCounter: isCounter, onRefresh: onRefresh, autoRefreshTime: autoRefreshTime, addProps: addProps && {
23473
+ React.createElement(TableTopButtonGroup, { isInSearch: isInSearch, onInSearch: onInSearch, isSelect: isSelect, isCounter: isCounter, onRefresh: onRefresh, autoRefreshTime: autoRefreshTime, addProps: addProps && {
23439
23474
  ...addProps,
23440
23475
  onFun: () => {
23441
23476
  if (addProps.customModal) {
@@ -23616,7 +23651,7 @@ const AntBaseTable = ({ bordered, className, rowKey, rowName, dataSource, initDa
23616
23651
  return {
23617
23652
  onClick: () => { }, // click header row
23618
23653
  };
23619
- }, dataSource: dataSource?.map((i) => i),
23654
+ }, dataSource: viewData?.map((i) => i),
23620
23655
  // columns={tableColumns(
23621
23656
  // columns.concat(
23622
23657
  // deleteCol({