@withwiz/gallery 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/README.ko.md +484 -0
- package/README.md +484 -0
- package/dist/chunk-B3MQL6ZR.mjs +79 -0
- package/dist/chunk-B3MQL6ZR.mjs.map +1 -0
- package/dist/chunk-BC3TPJQY.mjs +122 -0
- package/dist/chunk-BC3TPJQY.mjs.map +1 -0
- package/dist/chunk-BTLELFKC.mjs +11 -0
- package/dist/chunk-BTLELFKC.mjs.map +1 -0
- package/dist/chunk-GDUGKVDQ.mjs +50 -0
- package/dist/chunk-GDUGKVDQ.mjs.map +1 -0
- package/dist/chunk-GFOWZGJX.js +50 -0
- package/dist/chunk-GFOWZGJX.js.map +1 -0
- package/dist/chunk-ILSZPLME.js +47 -0
- package/dist/chunk-ILSZPLME.js.map +1 -0
- package/dist/chunk-M7CCTDUG.js +122 -0
- package/dist/chunk-M7CCTDUG.js.map +1 -0
- package/dist/chunk-WVRTM6E7.js +19 -0
- package/dist/chunk-WVRTM6E7.js.map +1 -0
- package/dist/chunk-XLPMNJJV.js +11 -0
- package/dist/chunk-XLPMNJJV.js.map +1 -0
- package/dist/chunk-XSPWM4DD.js +79 -0
- package/dist/chunk-XSPWM4DD.js.map +1 -0
- package/dist/chunk-YUMB3GBN.mjs +47 -0
- package/dist/chunk-YUMB3GBN.mjs.map +1 -0
- package/dist/chunk-ZEZCUJJD.mjs +19 -0
- package/dist/chunk-ZEZCUJJD.mjs.map +1 -0
- package/dist/components/gallery.css +1074 -0
- package/dist/components/index.d.mts +156 -0
- package/dist/components/index.d.ts +156 -0
- package/dist/components/index.js +1215 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +1215 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/errors-B9gzUsX-.d.ts +39 -0
- package/dist/errors-DYrlDCzP.d.mts +39 -0
- package/dist/gallery.css +1074 -0
- package/dist/hooks/index.d.mts +65 -0
- package/dist/hooks/index.d.ts +65 -0
- package/dist/hooks/index.js +14 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/index.mjs +14 -0
- package/dist/hooks/index.mjs.map +1 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +37 -0
- package/dist/index.mjs.map +1 -0
- package/dist/presets/ballet.d.mts +64 -0
- package/dist/presets/ballet.d.ts +64 -0
- package/dist/presets/ballet.js +162 -0
- package/dist/presets/ballet.js.map +1 -0
- package/dist/presets/ballet.mjs +162 -0
- package/dist/presets/ballet.mjs.map +1 -0
- package/dist/server/index.d.mts +157 -0
- package/dist/server/index.d.ts +157 -0
- package/dist/server/index.js +619 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +619 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/types/index.d.mts +137 -0
- package/dist/types/index.d.ts +137 -0
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.mjs +1 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/useGalleryLightbox-BYzPyBYB.d.mts +22 -0
- package/dist/useGalleryLightbox-BYzPyBYB.d.ts +22 -0
- package/dist/validators/index.d.mts +68 -0
- package/dist/validators/index.d.ts +68 -0
- package/dist/validators/index.js +7 -0
- package/dist/validators/index.js.map +1 -0
- package/dist/validators/index.mjs +7 -0
- package/dist/validators/index.mjs.map +1 -0
- package/package.json +85 -0
- package/prisma/gallery.schema.prisma +62 -0
- package/prisma/migrations/2026-05-24-enum-to-table.sql +65 -0
package/README.ko.md
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
# @withwiz/gallery
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | **한국어**
|
|
4
|
+
|
|
5
|
+
Next.js 16 + Prisma 7 호스트에 독립적으로 동작하는 갤러리 모듈. 자가완결형 어드민 UI, 헤드리스 라이트박스, 드롭인 API 라우트 팩토리를 제공한다.
|
|
6
|
+
|
|
7
|
+
## 상태
|
|
8
|
+
|
|
9
|
+
**v0.1.0** — Sprint 1–6 완료 (`@withwiz/gallery` 스캐폴딩 + validators + services + server layer + hooks/primitive UI + admin composite UI + Prisma partial + ballet preset + README). ballet 마이그레이션 (Sprint 7) 미실시.
|
|
10
|
+
|
|
11
|
+
## 주요 기능
|
|
12
|
+
|
|
13
|
+
- **헤드리스** — `setGalleryConfig` 한 번 호출로 어떤 Next.js 호스트에도 주입 (Prisma client / API wrapper / storage / revalidate / i18n / limits).
|
|
14
|
+
- **어드민 UI** — 5개 컴포지트 컴포넌트 (`GalleryAdminManager`, `GalleryEditForm`, `GalleryHomePreview`, `GalleryManagerLayout`, `CategoryAdminManager`) + 2개 프리미티브 (`ImageDropZone`, `ToggleSwitch`). 자가완결 3-pane 레이아웃.
|
|
15
|
+
- **API 라우트** — `createGalleryRoutes(config)` 가 6개 엔드포인트 그룹의 Next.js Route Handler 를 반환 (collection / item / publish toggle / bulk / category collection / category item).
|
|
16
|
+
- **RSC 로더** — `getGalleryItems`, `getFeaturedGalleries`, `getRecentGalleries`, `getGalleryCount` — 서버 컴포넌트 / 대시보드 용.
|
|
17
|
+
- **퍼블릭 프리셋** — `PublicGalleryMosaic` (presets/ballet) — 1~7장 적응형 모자이크 + 라이트박스.
|
|
18
|
+
- **헤드리스 라이트박스 훅** — `useGalleryLightbox` — ESC / Arrow 키 자동 바인딩 + wrap-around.
|
|
19
|
+
- **이미지 업로드 프리미티브** — `useImageDropZone` + `<ImageDropZone>` (호스트 측 validate + accept/maxSize).
|
|
20
|
+
- **스토리지 비종속** — R2 / S3 / 로컬 파일시스템 등 어디든 호스트가 `config.storage` 로 주입.
|
|
21
|
+
- **호스트 모델명 비종속** — Prisma `Gallery` 모델은 호스트의 User/Admin/Account 이름을 가정하지 않는다. `authorId: String` 컬럼만.
|
|
22
|
+
- **타입 에러 계층** — `GalleryNotFoundError`, `CategoryNotFoundError`, `CategoryInUseError`, `PermissionDeniedError` 등 호스트가 catch 후 자체 처리.
|
|
23
|
+
|
|
24
|
+
## 설치
|
|
25
|
+
|
|
26
|
+
현재는 모노레포 안에서 file: 참조로 사용한다 (npm publish 미실시):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# host root
|
|
30
|
+
npm install file:../node-packages/withwiz-gallery
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
배포 후에는:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @withwiz/gallery
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Peer dependencies
|
|
40
|
+
|
|
41
|
+
| 패키지 | 버전 범위 |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `next` | `>=16` |
|
|
44
|
+
| `react` / `react-dom` | `>=19` |
|
|
45
|
+
| `@prisma/client` | `>=7` |
|
|
46
|
+
| `zod` | `>=4` |
|
|
47
|
+
| `clsx` | `>=2` |
|
|
48
|
+
| `tailwind-merge` | `>=3` |
|
|
49
|
+
| `sonner` | `>=2` (선택) |
|
|
50
|
+
|
|
51
|
+
## 빠른 시작
|
|
52
|
+
|
|
53
|
+
### 1. Prisma schema 머지
|
|
54
|
+
|
|
55
|
+
host 의 `prisma/` 디렉토리에 본 패키지의 partial schema 를 복사 또는 symlink.
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
// host package.json
|
|
59
|
+
{
|
|
60
|
+
"prisma": { "schema": "./prisma" }
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# host 측 작업
|
|
66
|
+
cp node_modules/@withwiz/gallery/prisma/gallery.schema.prisma prisma/
|
|
67
|
+
npx prisma generate
|
|
68
|
+
npx prisma migrate dev --create-only
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
이 partial 은 `GalleryCategory`, `Gallery` 두 모델 + 인덱스 + `@@map` 을 정의한다. host 의 User 모델 이름과 무관하게 `authorId: String` 컬럼만 두므로 multi-file schema 머지 시 validate 가 통과한다.
|
|
72
|
+
|
|
73
|
+
ballet 처럼 enum 기반 기존 스키마에서 마이그레이션하는 경우 아래 [Migration from ballet enum-based schema](#migration-from-ballet-enum-based-schema) 섹션 참조.
|
|
74
|
+
|
|
75
|
+
### 2. `setGalleryConfig` (host bootstrap)
|
|
76
|
+
|
|
77
|
+
**module top-level 에서 호출해야 한다** (client component 가 mount 시 `getGalleryConfig()` 를 즉시 호출하므로).
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// host: lib/gallery-config.ts
|
|
81
|
+
import { setGalleryConfig } from "@withwiz/gallery/server";
|
|
82
|
+
import type { GalleryConfig } from "@withwiz/gallery/server";
|
|
83
|
+
import { prisma } from "./prisma";
|
|
84
|
+
import { withAdminApi } from "@/lib/api-middleware"; // host 의 인증 래퍼
|
|
85
|
+
import { isR2Enabled, collectR2Keys, deleteR2Keys } from "@/lib/r2";
|
|
86
|
+
import { revalidatePath } from "next/cache";
|
|
87
|
+
|
|
88
|
+
export const galleryConfig: GalleryConfig = {
|
|
89
|
+
prisma,
|
|
90
|
+
apiWrapper: withAdminApi,
|
|
91
|
+
authorIdFromContext: (ctx) => ctx.user!.id,
|
|
92
|
+
limits: {
|
|
93
|
+
maxFeatured: 7,
|
|
94
|
+
mosaicCount: 7,
|
|
95
|
+
batchMax: 20,
|
|
96
|
+
captionMaxLength: 200,
|
|
97
|
+
},
|
|
98
|
+
revalidate: revalidatePath,
|
|
99
|
+
revalidatePaths: ["/", "/home/v1"],
|
|
100
|
+
storage: {
|
|
101
|
+
isEnabled: isR2Enabled,
|
|
102
|
+
collectKeys: collectR2Keys,
|
|
103
|
+
deleteKeys: deleteR2Keys,
|
|
104
|
+
},
|
|
105
|
+
i18n: {
|
|
106
|
+
"admin.title": "갤러리 관리",
|
|
107
|
+
"admin.newButton": "새 이미지",
|
|
108
|
+
"form.caption": "캡션",
|
|
109
|
+
"form.save": "저장",
|
|
110
|
+
"form.cancel": "취소",
|
|
111
|
+
// ... 나머지 키는 @withwiz/gallery/types 의 GalleryI18nKey 참조
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
setGalleryConfig(galleryConfig);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 3. Route handler mount
|
|
119
|
+
|
|
120
|
+
각 엔드포인트마다 thin re-export.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
// host: app/api/admin/galleries/route.ts
|
|
124
|
+
import { createGalleryRoutes } from "@withwiz/gallery/server";
|
|
125
|
+
import { galleryConfig } from "@/lib/gallery-config";
|
|
126
|
+
|
|
127
|
+
const { collection } = createGalleryRoutes(galleryConfig);
|
|
128
|
+
export const { GET, POST, DELETE } = collection;
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// host: app/api/admin/galleries/[id]/route.ts
|
|
133
|
+
import { createGalleryRoutes } from "@withwiz/gallery/server";
|
|
134
|
+
import { galleryConfig } from "@/lib/gallery-config";
|
|
135
|
+
|
|
136
|
+
const { item } = createGalleryRoutes(galleryConfig);
|
|
137
|
+
export const { GET, PUT, DELETE } = item;
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// host: app/api/admin/galleries/[id]/publish/route.ts
|
|
142
|
+
import { createGalleryRoutes } from "@withwiz/gallery/server";
|
|
143
|
+
import { galleryConfig } from "@/lib/gallery-config";
|
|
144
|
+
|
|
145
|
+
const { publishToggle } = createGalleryRoutes(galleryConfig);
|
|
146
|
+
export const { PATCH } = publishToggle;
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// host: app/api/admin/galleries/bulk/route.ts
|
|
151
|
+
import { createGalleryRoutes } from "@withwiz/gallery/server";
|
|
152
|
+
import { galleryConfig } from "@/lib/gallery-config";
|
|
153
|
+
|
|
154
|
+
const { bulk } = createGalleryRoutes(galleryConfig);
|
|
155
|
+
export const { POST, PATCH } = bulk;
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// host: app/api/admin/gallery-categories/route.ts
|
|
160
|
+
import { createGalleryRoutes } from "@withwiz/gallery/server";
|
|
161
|
+
import { galleryConfig } from "@/lib/gallery-config";
|
|
162
|
+
|
|
163
|
+
const { categoryCollection } = createGalleryRoutes(galleryConfig);
|
|
164
|
+
export const { GET, POST } = categoryCollection;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
// host: app/api/admin/gallery-categories/[id]/route.ts
|
|
169
|
+
import { createGalleryRoutes } from "@withwiz/gallery/server";
|
|
170
|
+
import { galleryConfig } from "@/lib/gallery-config";
|
|
171
|
+
|
|
172
|
+
const { categoryItem } = createGalleryRoutes(galleryConfig);
|
|
173
|
+
export const { GET, PUT, DELETE } = categoryItem;
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 4. Admin UI mount
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
// host: app/admin/galleries/page.tsx
|
|
180
|
+
import { GalleryAdminManager } from "@withwiz/gallery/components";
|
|
181
|
+
import "@withwiz/gallery/components/gallery.css";
|
|
182
|
+
import "@/lib/gallery-config"; // setGalleryConfig 가 module side effect
|
|
183
|
+
|
|
184
|
+
export default function GalleryAdminPage() {
|
|
185
|
+
return <GalleryAdminManager />;
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
// host: app/admin/galleries/new/page.tsx
|
|
191
|
+
import { GalleryAdminManager } from "@withwiz/gallery/components";
|
|
192
|
+
import "@/lib/gallery-config";
|
|
193
|
+
|
|
194
|
+
export default function GalleryNewPage() {
|
|
195
|
+
return <GalleryAdminManager initialMode="new" />;
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
// host: app/admin/galleries/[id]/page.tsx
|
|
201
|
+
import { GalleryAdminManager } from "@withwiz/gallery/components";
|
|
202
|
+
import "@/lib/gallery-config";
|
|
203
|
+
|
|
204
|
+
export default async function GalleryEditPage({
|
|
205
|
+
params,
|
|
206
|
+
}: {
|
|
207
|
+
params: Promise<{ id: string }>;
|
|
208
|
+
}) {
|
|
209
|
+
const { id } = await params;
|
|
210
|
+
return <GalleryAdminManager initialSelectedId={id} />;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
// host: app/admin/gallery-categories/page.tsx
|
|
216
|
+
import { CategoryAdminManager } from "@withwiz/gallery/components";
|
|
217
|
+
import "@/lib/gallery-config";
|
|
218
|
+
|
|
219
|
+
export default function CategoryAdminPage() {
|
|
220
|
+
return <CategoryAdminManager />;
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 5. Public gallery (RSC + client)
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
// host: app/page.tsx (RSC)
|
|
228
|
+
import { PublicGalleryMosaic } from "@withwiz/gallery/presets/ballet";
|
|
229
|
+
import { getFeaturedGalleries } from "@withwiz/gallery/server";
|
|
230
|
+
import { galleryConfig } from "@/lib/gallery-config";
|
|
231
|
+
import "@withwiz/gallery/components/gallery.css";
|
|
232
|
+
|
|
233
|
+
export default async function HomePage() {
|
|
234
|
+
const featured = await getFeaturedGalleries(galleryConfig, 7);
|
|
235
|
+
const images = featured.map((g) => ({
|
|
236
|
+
src: g.imageUrl,
|
|
237
|
+
alt: g.caption ?? "갤러리",
|
|
238
|
+
}));
|
|
239
|
+
return (
|
|
240
|
+
<PublicGalleryMosaic
|
|
241
|
+
images={images}
|
|
242
|
+
count={7}
|
|
243
|
+
i18n={{ sectionLabel: "갤러리", moments: "공연의 순간들" }}
|
|
244
|
+
/>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## API 레퍼런스
|
|
250
|
+
|
|
251
|
+
### `@withwiz/gallery` (메인 entry)
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import {
|
|
255
|
+
setGalleryConfig,
|
|
256
|
+
getGalleryConfig,
|
|
257
|
+
cn,
|
|
258
|
+
getVariantUrl,
|
|
259
|
+
GalleryError,
|
|
260
|
+
GalleryNotFoundError,
|
|
261
|
+
CategoryNotFoundError,
|
|
262
|
+
CategoryInUseError,
|
|
263
|
+
PermissionDeniedError,
|
|
264
|
+
} from "@withwiz/gallery";
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### `@withwiz/gallery/server`
|
|
268
|
+
|
|
269
|
+
| Export | 시그니처 |
|
|
270
|
+
|---|---|
|
|
271
|
+
| `setGalleryConfig(config)` | DI setter — 모듈 top-level 에서 1회 호출 |
|
|
272
|
+
| `getGalleryConfig()` | 현재 등록된 config 반환 (미설정이면 throw) |
|
|
273
|
+
| `createGalleryService(config)` | CRUD 객체 (list / get / create / update / remove / bulk*) |
|
|
274
|
+
| `createCategoryService(config)` | 카테고리 CRUD 객체 (list / getBySlug / create / update / remove / reorder) |
|
|
275
|
+
| `createGalleryRoutes(config)` | Next.js Route Handler 6 그룹 (`collection / item / publishToggle / bulk / categoryCollection / categoryItem`) |
|
|
276
|
+
| `getGalleryItems(config, opts)` | RSC 페이지네이션 loader |
|
|
277
|
+
| `getFeaturedGalleries(config, limit?)` | RSC featured 목록 |
|
|
278
|
+
| `getRecentGalleries(config, limit)` | RSC 최근 항목 (dashboard) |
|
|
279
|
+
| `getGalleryCount(config, opts?)` | RSC 카운트 (dashboard) |
|
|
280
|
+
| `buildPaginatedResult(items, page, limit, total)` | helper |
|
|
281
|
+
|
|
282
|
+
타입 에러 (모두 server entry 에서도 re-export):
|
|
283
|
+
|
|
284
|
+
- `GalleryError` (base)
|
|
285
|
+
- `GalleryNotFoundError`
|
|
286
|
+
- `CategoryNotFoundError`
|
|
287
|
+
- `CategoryInUseError`
|
|
288
|
+
- `PermissionDeniedError`
|
|
289
|
+
|
|
290
|
+
### `@withwiz/gallery/components`
|
|
291
|
+
|
|
292
|
+
| Export | 설명 |
|
|
293
|
+
|---|---|
|
|
294
|
+
| `GalleryAdminManager` | 3-pane 어드민 마운트 포인트 (`initialMode? / initialSelectedId?`) |
|
|
295
|
+
| `GalleryManagerLayout` | 3-pane primitive (좌=list / 중=form / 우=preview) |
|
|
296
|
+
| `GalleryEditForm` | 단일/다중 모드 form (`value / multipleMode / onSubmit / onSubmitMany`) |
|
|
297
|
+
| `GalleryHomePreview` | 7-tile 모자이크 + drag 재정렬 + 별 토글 |
|
|
298
|
+
| `CategoryAdminManager` | 카테고리 CRUD UI |
|
|
299
|
+
| `ImageDropZone` | 이미지 드롭존 (accept / maxSize / multiple / disabled / validate) |
|
|
300
|
+
| `ToggleSwitch` | size sm/md/lg, ARIA switch role |
|
|
301
|
+
|
|
302
|
+
CSS (호스트가 명시 import):
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
import "@withwiz/gallery/components/gallery.css";
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### `@withwiz/gallery/hooks`
|
|
309
|
+
|
|
310
|
+
| Export | 시그니처 |
|
|
311
|
+
|---|---|
|
|
312
|
+
| `useGalleryLightbox(images)` | `{ isOpen, currentIndex, current, open, close, next, prev }` — ESC / Arrow 자동 바인딩, wrap-around |
|
|
313
|
+
| `useImageDropZone(opts)` | 헤드리스 드롭존 — `inputProps / containerProps / files / rejectedReasons / clear` |
|
|
314
|
+
| `useScrollReveal(opts?)` | IntersectionObserver 기반 fade-in — `{ ref, isVisible }` |
|
|
315
|
+
|
|
316
|
+
### `@withwiz/gallery/validators`
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import { createGallerySchemas } from "@withwiz/gallery/validators";
|
|
320
|
+
|
|
321
|
+
const schemas = createGallerySchemas({
|
|
322
|
+
batchMax: 20,
|
|
323
|
+
captionMaxLength: 200,
|
|
324
|
+
});
|
|
325
|
+
// schemas.CreateGallerySchema / UpdateGallerySchema / BatchCreateGallerySchema
|
|
326
|
+
// / BulkUpdateSchema / CreateCategorySchema / UpdateCategorySchema / ReorderCategorySchema
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### `@withwiz/gallery/types`
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
import type {
|
|
333
|
+
GalleryConfig,
|
|
334
|
+
GalleryI18nKey,
|
|
335
|
+
GallerySlotName,
|
|
336
|
+
GalleryListItem,
|
|
337
|
+
GalleryDetail,
|
|
338
|
+
GalleryCategoryItem,
|
|
339
|
+
CreateGalleryInput,
|
|
340
|
+
UpdateGalleryInput,
|
|
341
|
+
CreateCategoryInput,
|
|
342
|
+
UpdateCategoryInput,
|
|
343
|
+
PaginatedResult,
|
|
344
|
+
PaginationMeta,
|
|
345
|
+
SortOrder,
|
|
346
|
+
PrismaLike,
|
|
347
|
+
ApiContext,
|
|
348
|
+
RouteHandler,
|
|
349
|
+
ApiWrapper,
|
|
350
|
+
} from "@withwiz/gallery/types";
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### `@withwiz/gallery/presets/ballet`
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
import { PublicGalleryMosaic } from "@withwiz/gallery/presets/ballet";
|
|
357
|
+
import type { PublicGalleryMosaicProps } from "@withwiz/gallery/presets/ballet";
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Signature:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
function PublicGalleryMosaic(props: {
|
|
364
|
+
images: { src: string; alt?: string }[];
|
|
365
|
+
count?: number; // default 7
|
|
366
|
+
i18n?: {
|
|
367
|
+
sectionLabel?: string;
|
|
368
|
+
moments?: string;
|
|
369
|
+
expandAria?: string;
|
|
370
|
+
lbClose?: string;
|
|
371
|
+
lbPrev?: string;
|
|
372
|
+
lbNext?: string;
|
|
373
|
+
};
|
|
374
|
+
className?: string;
|
|
375
|
+
scrollReveal?: boolean; // default true
|
|
376
|
+
hideHeader?: boolean; // default false
|
|
377
|
+
}): JSX.Element | null;
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## 호스트 비종속 보증
|
|
381
|
+
|
|
382
|
+
- **`@withwiz/pms` import 0** — 본 패키지는 도메인 패키지에 의존하지 않는다.
|
|
383
|
+
- **`process.env` 직접 의존 0** — 환경 변수는 호스트가 읽어 `config.storage` 등에 binding.
|
|
384
|
+
- **호스트의 User 모델명 가정 없음** — Prisma `Gallery` 모델은 `authorId: String` 컬럼만 두고 `@relation` 정의하지 않는다.
|
|
385
|
+
- **모든 인프라는 `GalleryConfig` 로 주입** — Prisma client / API wrapper / storage / revalidate / i18n / limits 모두 주입.
|
|
386
|
+
- **Storage / auth / revalidate 는 호스트 책임** — 본 패키지는 키/경로만 다루고 실제 R2/S3/로컬 호출은 호스트의 `config.storage.deleteKeys` 등에 위임.
|
|
387
|
+
- **`presets/ballet` 에서 `next/image`, `next/navigation`, `useI18n` 직접 의존 없음** — 이미지는 plain `<img>`, 라벨은 props.
|
|
388
|
+
|
|
389
|
+
## CSS 커스터마이즈
|
|
390
|
+
|
|
391
|
+
호스트의 root CSS 에서 CSS variable 을 override.
|
|
392
|
+
|
|
393
|
+
```css
|
|
394
|
+
/* host: app/globals.css */
|
|
395
|
+
:root {
|
|
396
|
+
--gallery-accent: 212 175 55; /* RGB triplet (NOT hex) */
|
|
397
|
+
--gallery-bg: 10 10 10;
|
|
398
|
+
--gallery-fg: 254 254 254;
|
|
399
|
+
--gallery-border: 30 30 30;
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
본 패키지는 위 4 variable 을 `:where(.gallery-toggle, .gallery-dropzone, .gallery-manager, .gallery-edit-form, .gallery-home-preview, .gallery-category-admin, .gallery-public-mosaic, .gallery-lightbox, ...)` 안에 fallback 값으로 정의해 두므로, 호스트가 override 하지 않아도 즉시 동작한다 (다크 톤 / 골드 액센트 기본).
|
|
404
|
+
|
|
405
|
+
추가로 어드민 컴포넌트는 `config.ui.classNames[slot]` / `config.ui.slots[slot]` 으로 slot 별 커스터마이즈 가능.
|
|
406
|
+
|
|
407
|
+
## ballet enum 기반 스키마에서 마이그레이션
|
|
408
|
+
|
|
409
|
+
ballet 의 기존 schema:
|
|
410
|
+
|
|
411
|
+
```prisma
|
|
412
|
+
enum GalleryCategory { PERFORMANCE | REHEARSAL | ACTIVITY | ARTIST }
|
|
413
|
+
model Gallery {
|
|
414
|
+
category GalleryCategory
|
|
415
|
+
// ...
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
본 패키지의 새 schema 는 `gallery_categories` 테이블 + `Gallery.categoryId` FK 로 전환한다. 마이그레이션 단계:
|
|
420
|
+
|
|
421
|
+
1. host 의 `prisma/` 에 본 패키지의 `gallery.schema.prisma` 를 머지 (복사 또는 symlink).
|
|
422
|
+
2. `npx prisma migrate dev --create-only` 로 마이그레이션 SQL 자동 생성.
|
|
423
|
+
3. 생성된 SQL 에서 `DROP COLUMN category` / `DROP TYPE "GalleryCategory"` 부분을 수동 제거 (별도 후속 마이그레이션으로 분리).
|
|
424
|
+
4. `prisma/migrations/2026-05-24-enum-to-table.sql` (본 패키지가 동봉) 의 내용을 그 위치에 삽입 — 4개 카테고리 seed + `galleries.category_id` 백필 + PL/pgSQL 검증 블록.
|
|
425
|
+
5. `npx prisma migrate dev` 로 적용.
|
|
426
|
+
6. 검증 통과 시 두 번째 마이그레이션 (별도 파일) 에서 enum 컬럼/타입 DROP.
|
|
427
|
+
|
|
428
|
+
본 패키지의 `prisma/migrations/2026-05-24-enum-to-table.sql` 파일을 그대로 참조하라.
|
|
429
|
+
|
|
430
|
+
ballet 코드 (`src/components/sections/Gallery.tsx` 의 `gallery-mosaic` / `lightbox-*` 클래스) 와 본 패키지의 `gallery-public-mosaic` / `gallery-lightbox__*` 클래스는 prefix 가 다르므로 격리된다. ballet 마이그레이션 시 기존 main.css 의 해당 클래스를 제거하고 본 패키지의 `gallery.css` 만 import 하면 된다.
|
|
431
|
+
|
|
432
|
+
## 아키텍처
|
|
433
|
+
|
|
434
|
+
```
|
|
435
|
+
@withwiz/toolkit (가장 낮음)
|
|
436
|
+
↑
|
|
437
|
+
@withwiz/pms (공연관리 도메인 CMS)
|
|
438
|
+
↑ ↑
|
|
439
|
+
│ │ host 로서 끌어 씀
|
|
440
|
+
host app (ballet, yeroom) │
|
|
441
|
+
↓ │
|
|
442
|
+
└─→ @withwiz/gallery ┘ (이 패키지 — 어떤 호스트에도 의존하지 않음)
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
`@withwiz/gallery` 은 어떤 호스트 프로젝트 (ballet, yeroom 등)에도, 어떤 도메인 패키지 (`@withwiz/pms` 등)에도, 호스트의 도메인 모델 (`User`, `Admin`, `Account` 등) 에도 일절 의존하지 않는다. 호스트가 무엇을 쓰든 (Next 16 / Prisma 7 가정) `setGalleryConfig` 한 번으로 통합 가능하다.
|
|
446
|
+
|
|
447
|
+
## 저장소 구조
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
node-packages/withwiz-gallery/
|
|
451
|
+
├── package.json
|
|
452
|
+
├── tsup.config.ts
|
|
453
|
+
├── README.md
|
|
454
|
+
├── prisma/
|
|
455
|
+
│ ├── gallery.schema.prisma # GalleryCategory + Gallery 모델 partial
|
|
456
|
+
│ └── migrations/
|
|
457
|
+
│ └── 2026-05-24-enum-to-table.sql # ballet enum → table backfill
|
|
458
|
+
├── docs/superpowers/{specs,plans}/ # 설계 문서 + sprint contracts/reports
|
|
459
|
+
└── src/
|
|
460
|
+
├── index.ts # main entry — config, types, errors, utils
|
|
461
|
+
├── config.ts
|
|
462
|
+
├── errors.ts
|
|
463
|
+
├── types/ # GalleryConfig / domain / pagination / api-context / i18n
|
|
464
|
+
├── validators/ # createGallerySchemas
|
|
465
|
+
├── services/ # createGalleryService / createCategoryService / helpers
|
|
466
|
+
├── server/ # createGalleryRoutes + loaders + server re-exports
|
|
467
|
+
├── components/ # admin UI + ImageDropZone + ToggleSwitch + gallery.css
|
|
468
|
+
├── hooks/ # useGalleryLightbox / useImageDropZone / useScrollReveal
|
|
469
|
+
├── utils/ # cn / image-variants / api-helpers
|
|
470
|
+
└── presets/
|
|
471
|
+
└── ballet.tsx # PublicGalleryMosaic
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
## 스크립트
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
npm run build # tsup multi-entry CJS/ESM/DTS
|
|
478
|
+
npm test # vitest run (221 tests, 23 files)
|
|
479
|
+
npm run test:watch # vitest watch
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## 라이선스
|
|
483
|
+
|
|
484
|
+
MIT
|