gw-admin 0.1.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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +569 -0
- package/dist/account/admin-logout-button.d.ts +21 -0
- package/dist/account/admin-logout-button.d.ts.map +1 -0
- package/dist/account/admin-logout-button.js +57 -0
- package/dist/account/admin-logout-button.js.map +1 -0
- package/dist/account/index.d.ts +2 -0
- package/dist/account/index.d.ts.map +1 -0
- package/dist/account/index.js +3 -0
- package/dist/account/index.js.map +1 -0
- package/dist/attachments/admin-attachment-list.d.ts +12 -0
- package/dist/attachments/admin-attachment-list.d.ts.map +1 -0
- package/dist/attachments/admin-attachment-list.js +11 -0
- package/dist/attachments/admin-attachment-list.js.map +1 -0
- package/dist/attachments/admin-file-attachments.d.ts +33 -0
- package/dist/attachments/admin-file-attachments.d.ts.map +1 -0
- package/dist/attachments/admin-file-attachments.js +94 -0
- package/dist/attachments/admin-file-attachments.js.map +1 -0
- package/dist/attachments/index.d.ts +4 -0
- package/dist/attachments/index.d.ts.map +1 -0
- package/dist/attachments/index.js +3 -0
- package/dist/attachments/index.js.map +1 -0
- package/dist/attachments/types.d.ts +12 -0
- package/dist/attachments/types.d.ts.map +1 -0
- package/dist/attachments/types.js +2 -0
- package/dist/attachments/types.js.map +1 -0
- package/dist/crud/form/admin-crud-form.d.ts +41 -0
- package/dist/crud/form/admin-crud-form.d.ts.map +1 -0
- package/dist/crud/form/admin-crud-form.js +56 -0
- package/dist/crud/form/admin-crud-form.js.map +1 -0
- package/dist/crud/form/form-state.d.ts +12 -0
- package/dist/crud/form/form-state.d.ts.map +1 -0
- package/dist/crud/form/form-state.js +48 -0
- package/dist/crud/form/form-state.js.map +1 -0
- package/dist/crud/form/index.d.ts +3 -0
- package/dist/crud/form/index.d.ts.map +1 -0
- package/dist/crud/form/index.js +3 -0
- package/dist/crud/form/index.js.map +1 -0
- package/dist/crud/index.d.ts +3 -0
- package/dist/crud/index.d.ts.map +1 -0
- package/dist/crud/index.js +3 -0
- package/dist/crud/index.js.map +1 -0
- package/dist/crud/pages/admin-crud-detail-page.d.ts +33 -0
- package/dist/crud/pages/admin-crud-detail-page.d.ts.map +1 -0
- package/dist/crud/pages/admin-crud-detail-page.js +39 -0
- package/dist/crud/pages/admin-crud-detail-page.js.map +1 -0
- package/dist/crud/pages/admin-crud-form-pages.d.ts +27 -0
- package/dist/crud/pages/admin-crud-form-pages.d.ts.map +1 -0
- package/dist/crud/pages/admin-crud-form-pages.js +44 -0
- package/dist/crud/pages/admin-crud-form-pages.js.map +1 -0
- package/dist/crud/pages/admin-crud-list-page.d.ts +27 -0
- package/dist/crud/pages/admin-crud-list-page.d.ts.map +1 -0
- package/dist/crud/pages/admin-crud-list-page.js +33 -0
- package/dist/crud/pages/admin-crud-list-page.js.map +1 -0
- package/dist/crud/pages/admin-delete-form.d.ts +12 -0
- package/dist/crud/pages/admin-delete-form.d.ts.map +1 -0
- package/dist/crud/pages/admin-delete-form.js +15 -0
- package/dist/crud/pages/admin-delete-form.js.map +1 -0
- package/dist/crud/pages/format-admin-value.d.ts +5 -0
- package/dist/crud/pages/format-admin-value.d.ts.map +1 -0
- package/dist/crud/pages/format-admin-value.js +38 -0
- package/dist/crud/pages/format-admin-value.js.map +1 -0
- package/dist/crud/pages/index.d.ts +6 -0
- package/dist/crud/pages/index.d.ts.map +1 -0
- package/dist/crud/pages/index.js +6 -0
- package/dist/crud/pages/index.js.map +1 -0
- package/dist/crud/preset/client.d.ts +2 -0
- package/dist/crud/preset/client.d.ts.map +1 -0
- package/dist/crud/preset/client.js +3 -0
- package/dist/crud/preset/client.js.map +1 -0
- package/dist/crud/preset/create-admin-crud-form.d.ts +41 -0
- package/dist/crud/preset/create-admin-crud-form.d.ts.map +1 -0
- package/dist/crud/preset/create-admin-crud-form.js +68 -0
- package/dist/crud/preset/create-admin-crud-form.js.map +1 -0
- package/dist/crud/preset/create-admin-crud-pages.d.ts +29 -0
- package/dist/crud/preset/create-admin-crud-pages.d.ts.map +1 -0
- package/dist/crud/preset/create-admin-crud-pages.js +46 -0
- package/dist/crud/preset/create-admin-crud-pages.js.map +1 -0
- package/dist/crud/preset/create-admin-crud-route-page.d.ts +18 -0
- package/dist/crud/preset/create-admin-crud-route-page.d.ts.map +1 -0
- package/dist/crud/preset/create-admin-crud-route-page.js +26 -0
- package/dist/crud/preset/create-admin-crud-route-page.js.map +1 -0
- package/dist/crud/preset/index.d.ts +3 -0
- package/dist/crud/preset/index.d.ts.map +1 -0
- package/dist/crud/preset/index.js +3 -0
- package/dist/crud/preset/index.js.map +1 -0
- package/dist/header/admin-page-header.d.ts +15 -0
- package/dist/header/admin-page-header.d.ts.map +1 -0
- package/dist/header/admin-page-header.js +10 -0
- package/dist/header/admin-page-header.js.map +1 -0
- package/dist/header/index.d.ts +2 -0
- package/dist/header/index.d.ts.map +1 -0
- package/dist/header/index.js +2 -0
- package/dist/header/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/class-names.d.ts +5 -0
- package/dist/internal/class-names.d.ts.map +1 -0
- package/dist/internal/class-names.js +7 -0
- package/dist/internal/class-names.js.map +1 -0
- package/dist/layout/admin-layout.d.ts +16 -0
- package/dist/layout/admin-layout.d.ts.map +1 -0
- package/dist/layout/admin-layout.js +17 -0
- package/dist/layout/admin-layout.js.map +1 -0
- package/dist/layout/admin-shell.d.ts +12 -0
- package/dist/layout/admin-shell.d.ts.map +1 -0
- package/dist/layout/admin-shell.js +10 -0
- package/dist/layout/admin-shell.js.map +1 -0
- package/dist/layout/admin-sidebar.d.ts +11 -0
- package/dist/layout/admin-sidebar.d.ts.map +1 -0
- package/dist/layout/admin-sidebar.js +10 -0
- package/dist/layout/admin-sidebar.js.map +1 -0
- package/dist/layout/index.d.ts +4 -0
- package/dist/layout/index.d.ts.map +1 -0
- package/dist/layout/index.js +4 -0
- package/dist/layout/index.js.map +1 -0
- package/dist/layout/mobile.d.ts +15 -0
- package/dist/layout/mobile.d.ts.map +1 -0
- package/dist/layout/mobile.js +32 -0
- package/dist/layout/mobile.js.map +1 -0
- package/dist/navigation/admin-navigation.d.ts +21 -0
- package/dist/navigation/admin-navigation.d.ts.map +1 -0
- package/dist/navigation/admin-navigation.js +22 -0
- package/dist/navigation/admin-navigation.js.map +1 -0
- package/dist/navigation/index.d.ts +2 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +2 -0
- package/dist/navigation/index.js.map +1 -0
- package/dist/pages/admin-account-page.d.ts +13 -0
- package/dist/pages/admin-account-page.d.ts.map +1 -0
- package/dist/pages/admin-account-page.js +11 -0
- package/dist/pages/admin-account-page.js.map +1 -0
- package/dist/pages/admin-dashboard-page.d.ts +15 -0
- package/dist/pages/admin-dashboard-page.d.ts.map +1 -0
- package/dist/pages/admin-dashboard-page.js +8 -0
- package/dist/pages/admin-dashboard-page.js.map +1 -0
- package/dist/pages/index.d.ts +3 -0
- package/dist/pages/index.d.ts.map +1 -0
- package/dist/pages/index.js +3 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/rich-text/admin-rich-text-editor.d.ts +27 -0
- package/dist/rich-text/admin-rich-text-editor.d.ts.map +1 -0
- package/dist/rich-text/admin-rich-text-editor.js +102 -0
- package/dist/rich-text/admin-rich-text-editor.js.map +1 -0
- package/dist/rich-text/admin-rich-text-file.d.ts +24 -0
- package/dist/rich-text/admin-rich-text-file.d.ts.map +1 -0
- package/dist/rich-text/admin-rich-text-file.js +80 -0
- package/dist/rich-text/admin-rich-text-file.js.map +1 -0
- package/dist/rich-text/admin-rich-text-view.d.ts +9 -0
- package/dist/rich-text/admin-rich-text-view.d.ts.map +1 -0
- package/dist/rich-text/admin-rich-text-view.js +9 -0
- package/dist/rich-text/admin-rich-text-view.js.map +1 -0
- package/dist/rich-text/index.d.ts +5 -0
- package/dist/rich-text/index.d.ts.map +1 -0
- package/dist/rich-text/index.js +5 -0
- package/dist/rich-text/index.js.map +1 -0
- package/dist/rich-text/plain-text.d.ts +9 -0
- package/dist/rich-text/plain-text.d.ts.map +1 -0
- package/dist/rich-text/plain-text.js +24 -0
- package/dist/rich-text/plain-text.js.map +1 -0
- package/package.json +163 -0
- package/rich-text.css +1 -0
- package/styles.css +901 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
## 0.1.0 - 2026-07-31
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Composable admin shell, sidebar, mobile navigation, page header, and logout
|
|
10
|
+
action.
|
|
11
|
+
- Complete `AdminLayout`, dashboard, and account-page defaults for the common
|
|
12
|
+
path, while preserving the lower-level composition slots.
|
|
13
|
+
- Optional list, detail, create, update, delete, and store-backed CRUD form
|
|
14
|
+
components for `next-headless-crud`.
|
|
15
|
+
- CRUD preset factories that configure fields once, connect attachments and
|
|
16
|
+
rich text by convention, and build a catch-all route from resource/action
|
|
17
|
+
pairs.
|
|
18
|
+
- Application-injected attachment and rich-text upload adapters.
|
|
19
|
+
- Focused package exports so consumers can opt into individual features.
|
|
20
|
+
- Optional default styles, rich-text baseline styles, CSS variables, and
|
|
21
|
+
stable prefixed class names.
|
|
22
|
+
- npm publishing workflow, package verification, and usage documentation.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 oh-jinsu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
# gw-admin
|
|
2
|
+
|
|
3
|
+
Composable administration UI for the Next.js App Router and
|
|
4
|
+
`next-headless-crud`.
|
|
5
|
+
|
|
6
|
+
`gw-admin` supplies reusable UI without owning application routes,
|
|
7
|
+
authentication, database access, navigation data, brand assets, file storage,
|
|
8
|
+
or deployment. Every feature is available through a focused package subpath,
|
|
9
|
+
so an application can use the complete default UI, replace one page, or use
|
|
10
|
+
only a few primitives.
|
|
11
|
+
|
|
12
|
+
## Design goals
|
|
13
|
+
|
|
14
|
+
- Application-owned `app/admin/**` routes
|
|
15
|
+
- Replaceable desktop, mobile, page-header, and page-content slots
|
|
16
|
+
- Separate imports for CRUD pages, CRUD forms, attachments, rich text, and
|
|
17
|
+
account actions
|
|
18
|
+
- Service-owned authentication, Server Actions, repositories, and uploaders
|
|
19
|
+
- Optional default CSS with stable `gw-admin-*` class names and CSS variables
|
|
20
|
+
- Preserved React Server Component and Client Component boundaries
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- Node.js 20 or newer
|
|
25
|
+
- React 19 or newer
|
|
26
|
+
- Next.js 15 or newer when using the supplied Next.js components
|
|
27
|
+
|
|
28
|
+
Feature entry points have additional optional peers:
|
|
29
|
+
|
|
30
|
+
| Entry point | Additional packages |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `gw-admin/crud/pages` | `next-headless-crud` |
|
|
33
|
+
| `gw-admin/crud/form` | `next-headless-crud`, `gw-store`, `react-store-input` |
|
|
34
|
+
| `gw-admin/crud/preset` | Complete CRUD peer set below |
|
|
35
|
+
| `gw-admin/crud/preset/client` | Complete CRUD peer set below |
|
|
36
|
+
| `gw-admin/attachments` | `gw-store`, `gw-react-file-input` |
|
|
37
|
+
| `gw-admin/rich-text` | `gw-store`, `react-store-input`, `gw-rich-text-editor` |
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
Install the core package:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install gw-admin
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Install only the peers required by the features the application uses. For the
|
|
48
|
+
complete CRUD, attachment, and editor stack:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install next-headless-crud gw-store react-store-input \
|
|
52
|
+
gw-react-file-input gw-rich-text-editor
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The package ships compiled JavaScript and declarations. A consuming Next.js
|
|
56
|
+
application does not need `transpilePackages`.
|
|
57
|
+
|
|
58
|
+
## Recommended setup
|
|
59
|
+
|
|
60
|
+
The preset API keeps the normal application integration to three small files.
|
|
61
|
+
The application supplies only its brand and navigation, a storage uploader and
|
|
62
|
+
resolver, and its CRUD resources and Server Actions.
|
|
63
|
+
|
|
64
|
+
### 1. Layout
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// app/admin/layout.tsx
|
|
68
|
+
import Image from "next/image";
|
|
69
|
+
import { AdminLayout } from "gw-admin";
|
|
70
|
+
import "gw-admin/styles.css";
|
|
71
|
+
import "gw-admin/rich-text.css";
|
|
72
|
+
|
|
73
|
+
const navigation = [
|
|
74
|
+
{ label: "홈", href: "/admin" },
|
|
75
|
+
{ label: "새소식", href: "/admin/news" },
|
|
76
|
+
{ label: "계정", href: "/admin/account" },
|
|
77
|
+
] as const;
|
|
78
|
+
|
|
79
|
+
export default async function Layout({ children }: { children: React.ReactNode }) {
|
|
80
|
+
await requireAdmin();
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<AdminLayout
|
|
84
|
+
brand={<Image src="/logo.png" alt="서비스" width={100} height={100} />}
|
|
85
|
+
navigation={navigation}
|
|
86
|
+
>
|
|
87
|
+
{children}
|
|
88
|
+
</AdminLayout>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 2. One client-side form preset
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
// admin/admin-form.tsx
|
|
97
|
+
"use client";
|
|
98
|
+
|
|
99
|
+
import { createAdminCrudForm } from "gw-admin/crud/preset/client";
|
|
100
|
+
import { uploadFile } from "@/files/upload-file";
|
|
101
|
+
|
|
102
|
+
export const AdminForm = createAdminCrudForm({
|
|
103
|
+
upload: uploadFile,
|
|
104
|
+
hiddenFields: ["slug", "summary", "publishedAt"],
|
|
105
|
+
hiddenFieldsByPath: {
|
|
106
|
+
"/admin/inquiries": ["status", "repliedAt"],
|
|
107
|
+
},
|
|
108
|
+
createDefaults: { isPrivate: true },
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
By convention the preset renders `attachmentFileIds` as an attachment field
|
|
113
|
+
and `content` and `replyContent` as rich-text fields. Every name and behavior
|
|
114
|
+
can be changed through the preset options.
|
|
115
|
+
|
|
116
|
+
### 3. One catch-all CRUD route
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
// app/admin/[...path]/page.tsx
|
|
120
|
+
import {
|
|
121
|
+
createAdminCrudPages,
|
|
122
|
+
createAdminCrudRoutePage,
|
|
123
|
+
} from "gw-admin/crud/preset";
|
|
124
|
+
import { AdminForm } from "@/admin/admin-form";
|
|
125
|
+
import { resolveFiles } from "@/files/resolve-files";
|
|
126
|
+
|
|
127
|
+
const pages = createAdminCrudPages({
|
|
128
|
+
form: AdminForm,
|
|
129
|
+
resolveFiles,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
export default createAdminCrudRoutePage({
|
|
133
|
+
pathPrefix: "admin",
|
|
134
|
+
pages,
|
|
135
|
+
routes: [
|
|
136
|
+
{
|
|
137
|
+
resource: newsResource,
|
|
138
|
+
actions: { create: createNews, update: updateNews, delete: deleteNews },
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
resource: inquiriesResource,
|
|
142
|
+
actions: {
|
|
143
|
+
create: createInquiry,
|
|
144
|
+
update: updateInquiry,
|
|
145
|
+
delete: deleteInquiry,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`uploadFile` and `resolveFiles` use the small storage-neutral types exported by
|
|
153
|
+
`gw-admin/attachments`; the package does not require a particular API, CDN, or
|
|
154
|
+
database.
|
|
155
|
+
|
|
156
|
+
The preset is optional. Low-level entry points remain public for applications
|
|
157
|
+
that need only one component or want to own the page composition.
|
|
158
|
+
|
|
159
|
+
To replace one page for one resource, add only that override to the route:
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
{
|
|
163
|
+
resource: reportsResource,
|
|
164
|
+
actions: reportActions,
|
|
165
|
+
pages: { detail: ReportsDetailPage },
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
To opt out at the field level, use `renderField` in `createAdminCrudForm` or
|
|
170
|
+
`renderDetailField` in `createAdminCrudPages`. Returning `undefined` delegates
|
|
171
|
+
back to the preset.
|
|
172
|
+
|
|
173
|
+
## Entry points
|
|
174
|
+
|
|
175
|
+
| Import | Purpose |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| `gw-admin` | Server-safe shell, sidebar, navigation, and page header |
|
|
178
|
+
| `gw-admin/layout` | Shell and desktop sidebar |
|
|
179
|
+
| `gw-admin/layout/mobile` | Optional client-side mobile header and drawer |
|
|
180
|
+
| `gw-admin/header` | Breadcrumb page header |
|
|
181
|
+
| `gw-admin/navigation` | Application-owned navigation rendering |
|
|
182
|
+
| `gw-admin/account` | Configurable logout button |
|
|
183
|
+
| `gw-admin/pages` | Dashboard and account pages |
|
|
184
|
+
| `gw-admin/crud/pages` | List, detail, create, update, and delete UI |
|
|
185
|
+
| `gw-admin/crud/form` | Store-backed standard CRUD form |
|
|
186
|
+
| `gw-admin/crud/preset` | Shared CRUD pages and catch-all route factory |
|
|
187
|
+
| `gw-admin/crud/preset/client` | Client-side form preset factory |
|
|
188
|
+
| `gw-admin/attachments` | Attachment list and uploader field |
|
|
189
|
+
| `gw-admin/rich-text` | Store-backed editor, view, nodes, and upload adapter |
|
|
190
|
+
| `gw-admin/styles.css` | Optional admin UI theme |
|
|
191
|
+
| `gw-admin/rich-text.css` | Optional rich-editor behavioral baseline |
|
|
192
|
+
|
|
193
|
+
Import focused subpaths in shared libraries and large applications. This keeps
|
|
194
|
+
optional feature dependencies outside bundles that do not use them.
|
|
195
|
+
|
|
196
|
+
## Application-owned layout
|
|
197
|
+
|
|
198
|
+
The application chooses its own brand and navigation.
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
// app/admin/layout.tsx
|
|
202
|
+
import Image from "next/image";
|
|
203
|
+
import {
|
|
204
|
+
AdminNavigation,
|
|
205
|
+
AdminShell,
|
|
206
|
+
AdminSidebar,
|
|
207
|
+
type AdminNavigationItem,
|
|
208
|
+
} from "gw-admin";
|
|
209
|
+
import { AdminMobileHeader } from "gw-admin/layout/mobile";
|
|
210
|
+
import "gw-admin/styles.css";
|
|
211
|
+
|
|
212
|
+
const navigation: readonly AdminNavigationItem[] = [
|
|
213
|
+
{ label: "홈", href: "/admin" },
|
|
214
|
+
{ label: "새소식", href: "/admin/news" },
|
|
215
|
+
{ label: "계정", href: "/admin/account" },
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
const brand = <Image src="/logo.png" alt="서비스 이름" width={100} height={100} />;
|
|
219
|
+
|
|
220
|
+
export default async function AdminLayout({
|
|
221
|
+
children,
|
|
222
|
+
}: {
|
|
223
|
+
children: React.ReactNode;
|
|
224
|
+
}) {
|
|
225
|
+
const session = await requireAdmin();
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<AdminShell
|
|
229
|
+
sidebar={
|
|
230
|
+
<AdminSidebar brand={brand}>
|
|
231
|
+
<AdminNavigation items={navigation} />
|
|
232
|
+
</AdminSidebar>
|
|
233
|
+
}
|
|
234
|
+
mobileHeader={
|
|
235
|
+
<AdminMobileHeader brand={brand} navigation={navigation} />
|
|
236
|
+
}
|
|
237
|
+
>
|
|
238
|
+
{children}
|
|
239
|
+
</AdminShell>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`AdminShell` does not create either navigation surface by itself:
|
|
245
|
+
|
|
246
|
+
- Pass no `sidebar` to omit the desktop sidebar.
|
|
247
|
+
- Pass no `mobileHeader` to omit the mobile header.
|
|
248
|
+
- Pass an application component into either slot to replace the default.
|
|
249
|
+
- Skip `AdminShell` entirely and import only `AdminPageHeader` or a CRUD page.
|
|
250
|
+
|
|
251
|
+
Authentication must remain in the application layout or page. Rendering an
|
|
252
|
+
admin component is not an authorization check.
|
|
253
|
+
|
|
254
|
+
## Logout
|
|
255
|
+
|
|
256
|
+
The default HTTP flow is configurable and performs a full navigation after
|
|
257
|
+
success so cached authenticated UI is discarded:
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
import { AdminLogoutButton } from "gw-admin/account";
|
|
261
|
+
|
|
262
|
+
<AdminLogoutButton
|
|
263
|
+
endpoint="/api/auth/logout"
|
|
264
|
+
redirectTo="/login"
|
|
265
|
+
/>;
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
For a non-HTTP client flow, wrap the button in an application Client Component
|
|
269
|
+
and supply `request`:
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
"use client";
|
|
273
|
+
|
|
274
|
+
import { AdminLogoutButton } from "gw-admin/account";
|
|
275
|
+
import { authClient } from "@/auth/client";
|
|
276
|
+
|
|
277
|
+
export function ServiceLogoutButton() {
|
|
278
|
+
return (
|
|
279
|
+
<AdminLogoutButton
|
|
280
|
+
request={() => authClient.logout()}
|
|
281
|
+
redirectTo="/sign-in"
|
|
282
|
+
/>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## CRUD pages
|
|
288
|
+
|
|
289
|
+
The package builds on `next-headless-crud`; resources and Server Actions still
|
|
290
|
+
belong to the application.
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
// app/admin/[...path]/page.tsx
|
|
294
|
+
import { createCrudRoutePage, defineCrudRoute } from "next-headless-crud/next";
|
|
295
|
+
import {
|
|
296
|
+
AdminCrudCreatePage,
|
|
297
|
+
AdminCrudDetailPage,
|
|
298
|
+
AdminCrudListPage,
|
|
299
|
+
AdminCrudUpdatePage,
|
|
300
|
+
} from "gw-admin/crud/pages";
|
|
301
|
+
import { newsResource } from "@/admin/news/resource";
|
|
302
|
+
import {
|
|
303
|
+
createNews,
|
|
304
|
+
deleteNews,
|
|
305
|
+
updateNews,
|
|
306
|
+
} from "@/admin/news/actions";
|
|
307
|
+
|
|
308
|
+
const newsRoute = defineCrudRoute({
|
|
309
|
+
resource: newsResource,
|
|
310
|
+
actions: {
|
|
311
|
+
create: createNews,
|
|
312
|
+
update: updateNews,
|
|
313
|
+
delete: deleteNews,
|
|
314
|
+
},
|
|
315
|
+
pages: {
|
|
316
|
+
list: AdminCrudListPage,
|
|
317
|
+
create: AdminCrudCreatePage,
|
|
318
|
+
detail: AdminCrudDetailPage,
|
|
319
|
+
update: AdminCrudUpdatePage,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
export default createCrudRoutePage([newsRoute]);
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Per-page opt-out
|
|
327
|
+
|
|
328
|
+
Next.js gives an explicit page precedence over a catch-all route. A custom page
|
|
329
|
+
can therefore replace one package page without changing the remaining CRUD
|
|
330
|
+
routes:
|
|
331
|
+
|
|
332
|
+
```text
|
|
333
|
+
app/admin/[...path]/page.tsx -> shared CRUD pages
|
|
334
|
+
app/admin/account/page.tsx -> application-owned page
|
|
335
|
+
app/admin/reports/page.tsx -> application-owned page
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Routes can also mix page implementations in one resource:
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
pages: {
|
|
342
|
+
list: AdminCrudListPage,
|
|
343
|
+
create: ServiceCreatePage,
|
|
344
|
+
detail: AdminCrudDetailPage,
|
|
345
|
+
update: ServiceUpdatePage,
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Detail field opt-out
|
|
350
|
+
|
|
351
|
+
Hide fields or replace rendering one field at a time:
|
|
352
|
+
|
|
353
|
+
```tsx
|
|
354
|
+
import {
|
|
355
|
+
AdminCrudDetailPage,
|
|
356
|
+
type AdminCrudDetailPageProps,
|
|
357
|
+
} from "gw-admin/crud/pages";
|
|
358
|
+
import { AdminRichTextView } from "gw-admin/rich-text";
|
|
359
|
+
|
|
360
|
+
export function NewsDetailPage(props: AdminCrudDetailPageProps) {
|
|
361
|
+
return (
|
|
362
|
+
<AdminCrudDetailPage
|
|
363
|
+
{...props}
|
|
364
|
+
hiddenFields={["description"]}
|
|
365
|
+
longTextFields={["content"]}
|
|
366
|
+
renderField={({ field, value }) => {
|
|
367
|
+
if (field.name === "content" && typeof value === "string") {
|
|
368
|
+
return <AdminRichTextView value={value} />;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return undefined;
|
|
372
|
+
}}
|
|
373
|
+
/>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
`renderActions` can replace every header action, while `formatValue` can
|
|
379
|
+
replace scalar formatting across the page.
|
|
380
|
+
|
|
381
|
+
## Store-backed form
|
|
382
|
+
|
|
383
|
+
`AdminCrudForm` handles standard input, select, checkbox, and textarea fields.
|
|
384
|
+
It deliberately does not guess that a field name represents an attachment,
|
|
385
|
+
editor, derived value, or server-managed value.
|
|
386
|
+
|
|
387
|
+
```tsx
|
|
388
|
+
"use client";
|
|
389
|
+
|
|
390
|
+
import {
|
|
391
|
+
AdminCrudForm,
|
|
392
|
+
type AdminCrudFormProps,
|
|
393
|
+
} from "gw-admin/crud/form";
|
|
394
|
+
|
|
395
|
+
export function ServiceAdminForm(props: AdminCrudFormProps) {
|
|
396
|
+
return (
|
|
397
|
+
<AdminCrudForm
|
|
398
|
+
{...props}
|
|
399
|
+
fieldModes={{
|
|
400
|
+
description: "textarea",
|
|
401
|
+
slug: "hidden",
|
|
402
|
+
publishedAt: "hidden",
|
|
403
|
+
}}
|
|
404
|
+
/>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
The save action is a `type="button"` action. Pressing Enter in a large input
|
|
410
|
+
screen does not implicitly submit the form.
|
|
411
|
+
|
|
412
|
+
### Composing attachment and editor fields
|
|
413
|
+
|
|
414
|
+
Feature adapters are created in the consuming application, not in the package.
|
|
415
|
+
This example is a Client Component, so uploader functions never cross the
|
|
416
|
+
Server Component serialization boundary:
|
|
417
|
+
|
|
418
|
+
```tsx
|
|
419
|
+
"use client";
|
|
420
|
+
|
|
421
|
+
import {
|
|
422
|
+
AdminCrudForm,
|
|
423
|
+
type AdminCrudFormProps,
|
|
424
|
+
} from "gw-admin/crud/form";
|
|
425
|
+
import {
|
|
426
|
+
AdminFileAttachments,
|
|
427
|
+
type AdminFileUploader,
|
|
428
|
+
type AdminUploadedFile,
|
|
429
|
+
} from "gw-admin/attachments";
|
|
430
|
+
import {
|
|
431
|
+
AdminRichTextEditor,
|
|
432
|
+
adminRichTextNodes,
|
|
433
|
+
createAdminRichTextUploadAdapter,
|
|
434
|
+
} from "gw-admin/rich-text";
|
|
435
|
+
import "gw-admin/rich-text.css";
|
|
436
|
+
|
|
437
|
+
const uploadFile: AdminFileUploader = async (file, context) => {
|
|
438
|
+
const uploaded = await serviceFileUploader(file, context);
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
id: uploaded.id,
|
|
442
|
+
name: uploaded.name,
|
|
443
|
+
type: uploaded.type,
|
|
444
|
+
size: uploaded.size,
|
|
445
|
+
key: uploaded.key,
|
|
446
|
+
url: cdn(uploaded.key),
|
|
447
|
+
metadata: uploaded.metadata,
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const editorUpload = createAdminRichTextUploadAdapter(uploadFile);
|
|
452
|
+
|
|
453
|
+
export function ServiceAdminForm(
|
|
454
|
+
props: AdminCrudFormProps & {
|
|
455
|
+
attachments: readonly AdminUploadedFile[];
|
|
456
|
+
},
|
|
457
|
+
) {
|
|
458
|
+
return (
|
|
459
|
+
<AdminCrudForm
|
|
460
|
+
{...props}
|
|
461
|
+
fieldModes={{ slug: "hidden" }}
|
|
462
|
+
renderField={({ field, store, error, errorId, setUploading }) => {
|
|
463
|
+
if (field.name === "attachmentFileIds") {
|
|
464
|
+
return (
|
|
465
|
+
<AdminFileAttachments
|
|
466
|
+
store={store}
|
|
467
|
+
name={field.name}
|
|
468
|
+
uploader={uploadFile}
|
|
469
|
+
initialFiles={props.attachments}
|
|
470
|
+
error={error}
|
|
471
|
+
onUploadingChange={setUploading}
|
|
472
|
+
/>
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (field.name === "content") {
|
|
477
|
+
return (
|
|
478
|
+
<AdminRichTextEditor
|
|
479
|
+
store={store}
|
|
480
|
+
name={field.name}
|
|
481
|
+
upload={editorUpload}
|
|
482
|
+
nodes={adminRichTextNodes}
|
|
483
|
+
invalid={Boolean(error)}
|
|
484
|
+
describedBy={error ? errorId : undefined}
|
|
485
|
+
/>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return undefined;
|
|
490
|
+
}}
|
|
491
|
+
/>
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
An application that does not need uploads simply does not install or import the
|
|
497
|
+
attachment entry point. Omitting `upload` from `AdminRichTextEditor` also
|
|
498
|
+
removes the attachment toolbar action.
|
|
499
|
+
|
|
500
|
+
## Styling
|
|
501
|
+
|
|
502
|
+
Default theme:
|
|
503
|
+
|
|
504
|
+
```tsx
|
|
505
|
+
import "gw-admin/styles.css";
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Editor behavioral baseline, only when the rich-text entry point is used:
|
|
509
|
+
|
|
510
|
+
```tsx
|
|
511
|
+
import "gw-admin/rich-text.css";
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
The package never imports these files automatically. To own every style, omit
|
|
515
|
+
both imports and target the stable `gw-admin-*` class names yourself.
|
|
516
|
+
|
|
517
|
+
The default theme can also be adjusted without replacing selectors:
|
|
518
|
+
|
|
519
|
+
```css
|
|
520
|
+
:root {
|
|
521
|
+
--gw-admin-background: #fafafa;
|
|
522
|
+
--gw-admin-surface: #ffffff;
|
|
523
|
+
--gw-admin-text: #111111;
|
|
524
|
+
--gw-admin-primary: #0b4d3b;
|
|
525
|
+
--gw-admin-primary-text: #ffffff;
|
|
526
|
+
--gw-admin-danger: #a61b1b;
|
|
527
|
+
--gw-admin-border: #d8d8d8;
|
|
528
|
+
--gw-admin-sidebar-width: 17rem;
|
|
529
|
+
--gw-admin-content-width: 90rem;
|
|
530
|
+
--gw-admin-radius: 0;
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
## Server and client boundaries
|
|
535
|
+
|
|
536
|
+
- Layout shell, sidebar, navigation, header, list page, detail page, and page
|
|
537
|
+
loaders are server-compatible.
|
|
538
|
+
- Mobile navigation, logout, delete, CRUD form, attachment input, and editor
|
|
539
|
+
are Client Components.
|
|
540
|
+
- Application upload functions must be declared in an application Client
|
|
541
|
+
Component before they are passed to attachment or editor components.
|
|
542
|
+
- Authentication, authorization, resource scoping, and Server Actions must
|
|
543
|
+
validate permissions independently of the rendered UI.
|
|
544
|
+
|
|
545
|
+
## Publishing and updates
|
|
546
|
+
|
|
547
|
+
The repository includes a manual-compatible npm publishing workflow. Configure
|
|
548
|
+
`NPM_PUBLISH_TOKEN` in the GitHub repository, bump the package version, and
|
|
549
|
+
push the package metadata to `main`.
|
|
550
|
+
|
|
551
|
+
```bash
|
|
552
|
+
npm version patch
|
|
553
|
+
git push --follow-tags
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
Before publishing:
|
|
557
|
+
|
|
558
|
+
```bash
|
|
559
|
+
npm run check
|
|
560
|
+
npm run pack:check
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Consuming services update manually:
|
|
564
|
+
|
|
565
|
+
```bash
|
|
566
|
+
npm install gw-admin@0.1.1
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
No automatic pull request or consumer deployment is performed.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type AdminLogoutRequest = () => Promise<Response | void>;
|
|
2
|
+
export type AdminLogoutButtonLabels = {
|
|
3
|
+
idle: string;
|
|
4
|
+
pending: string;
|
|
5
|
+
confirm: string;
|
|
6
|
+
error: string;
|
|
7
|
+
};
|
|
8
|
+
export type AdminLogoutButtonProps = {
|
|
9
|
+
endpoint?: string;
|
|
10
|
+
redirectTo?: string;
|
|
11
|
+
request?: AdminLogoutRequest;
|
|
12
|
+
confirm?: boolean;
|
|
13
|
+
labels?: Partial<AdminLogoutButtonLabels>;
|
|
14
|
+
className?: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Ends an admin session through either a conventional HTTP endpoint or a
|
|
18
|
+
* consumer-supplied client request.
|
|
19
|
+
*/
|
|
20
|
+
export declare function AdminLogoutButton({ endpoint, redirectTo, request, confirm, labels: labelOverrides, className, }: AdminLogoutButtonProps): import("react").JSX.Element;
|
|
21
|
+
//# sourceMappingURL=admin-logout-button.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-logout-button.d.ts","sourceRoot":"","sources":["../../src/account/admin-logout-button.tsx"],"names":[],"mappings":"AAKA,MAAM,MAAM,kBAAkB,GAAG,MAAM,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;AAEhE,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAoBF;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,QAA6B,EAC7B,UAAqB,EACrB,OAAO,EACP,OAAc,EACd,MAAM,EAAE,cAAc,EACtB,SAAS,GACV,EAAE,sBAAsB,+BAmDxB"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { classNames } from "../internal/class-names.js";
|
|
5
|
+
const defaultLabels = {
|
|
6
|
+
idle: "로그아웃",
|
|
7
|
+
pending: "로그아웃 중...",
|
|
8
|
+
confirm: "로그아웃하시겠습니까?",
|
|
9
|
+
error: "로그아웃하지 못했습니다. 잠시 후 다시 시도해 주세요.",
|
|
10
|
+
};
|
|
11
|
+
async function getResponseError(response, fallback) {
|
|
12
|
+
try {
|
|
13
|
+
const payload = (await response.json());
|
|
14
|
+
return typeof payload.message === "string" && payload.message
|
|
15
|
+
? payload.message
|
|
16
|
+
: fallback;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Ends an admin session through either a conventional HTTP endpoint or a
|
|
24
|
+
* consumer-supplied client request.
|
|
25
|
+
*/
|
|
26
|
+
export function AdminLogoutButton({ endpoint = "/api/auth/logout", redirectTo = "/login", request, confirm = true, labels: labelOverrides, className, }) {
|
|
27
|
+
const labels = { ...defaultLabels, ...labelOverrides };
|
|
28
|
+
const [pending, setPending] = useState(false);
|
|
29
|
+
const [errorMessage, setErrorMessage] = useState("");
|
|
30
|
+
const handleLogout = async () => {
|
|
31
|
+
if (confirm && !window.confirm(labels.confirm)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
setPending(true);
|
|
35
|
+
setErrorMessage("");
|
|
36
|
+
try {
|
|
37
|
+
const response = request
|
|
38
|
+
? await request()
|
|
39
|
+
: await fetch(endpoint, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
credentials: "same-origin",
|
|
42
|
+
});
|
|
43
|
+
if (response instanceof Response && !response.ok) {
|
|
44
|
+
setErrorMessage(await getResponseError(response, labels.error));
|
|
45
|
+
setPending(false);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
window.location.replace(redirectTo);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
setErrorMessage(error instanceof Error && error.message ? error.message : labels.error);
|
|
52
|
+
setPending(false);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return (_jsxs("div", { className: "gw-admin-logout", children: [_jsx("button", { type: "button", disabled: pending, onClick: handleLogout, className: classNames("gw-admin-button gw-admin-button--primary", className), children: pending ? labels.pending : labels.idle }), errorMessage && (_jsx("p", { role: "alert", className: "gw-admin-field-error", children: errorMessage }))] }));
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=admin-logout-button.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-logout-button.js","sourceRoot":"","sources":["../../src/account/admin-logout-button.tsx"],"names":[],"mappings":"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"}
|