@withwiz/gallery 0.1.0 → 0.2.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 +20 -1
- package/README.md +20 -1
- package/dist/chunk-IEXSUEDO.mjs +78 -0
- package/dist/chunk-IEXSUEDO.mjs.map +1 -0
- package/dist/chunk-IYQOJJBC.js +78 -0
- package/dist/chunk-IYQOJJBC.js.map +1 -0
- package/dist/server/index.d.mts +4 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +67 -8
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +62 -3
- package/dist/server/index.mjs.map +1 -1
- package/dist/types/index.d.mts +15 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/validators/index.d.mts +17 -3
- package/dist/validators/index.d.ts +17 -3
- package/dist/validators/index.js +8 -2
- package/dist/validators/index.js.map +1 -1
- package/dist/validators/index.mjs +7 -1
- package/package.json +9 -3
- package/dist/chunk-ILSZPLME.js +0 -47
- package/dist/chunk-ILSZPLME.js.map +0 -1
- package/dist/chunk-YUMB3GBN.mjs +0 -47
- package/dist/chunk-YUMB3GBN.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/services/helpers.ts","../../src/services/category-service.ts","../../src/services/index.ts","../../src/server/route-handlers.ts","../../src/utils/api-helpers.ts","../../src/server/loaders.ts"],"sourcesContent":["import type { PaginatedResult } from \"../types/pagination\";\n\n/**\n * pagination math 를 일관되게 처리. ballet 의 `buildPaginatedResult` 와 동일 시맨틱.\n *\n * - total === 0 일 때 totalPages = 0 (clamp 안 함; UI 에서 \"결과 없음\" 로 처리)\n * - page > totalPages 여도 결과 자체는 caller 의 query 가 결정 — meta 만 계산\n */\nexport function buildPaginatedResult<T>(\n items: T[],\n page: number,\n limit: number,\n total: number,\n): PaginatedResult<T> {\n const totalPages = total === 0 ? 0 : Math.ceil(total / limit);\n return {\n items,\n meta: {\n page,\n limit,\n total,\n totalPages,\n hasNext: page < totalPages,\n hasPrev: page > 1,\n },\n };\n}\n","import type {\n GalleryConfig,\n GalleryCategoryItem,\n CreateCategoryInput,\n UpdateCategoryInput,\n} from \"../types\";\nimport { CategoryInUseError } from \"../errors\";\n\n/**\n * spec §9-2 의 categoryService.\n *\n * 호스트 독립성을 위해 Prisma delegate 는 `config.prisma[modelName]` 패턴으로 접근한다\n * (modelName 기본값 = \"galleryCategory\", gallery 본체 modelName 기본값 = \"gallery\").\n *\n * remove() 는 `onDelete: Restrict` 보강 — 사전 gallery.count() 후 throw.\n * admin UX 가 친화적 에러 메시지를 표시하도록 명시적 throw 를 사용.\n */\nexport function createCategoryService(config: GalleryConfig) {\n const categoryDelegate = () =>\n config.prisma[config.categoryModelName ?? \"galleryCategory\"];\n const galleryDelegate = () => config.prisma[config.modelName ?? \"gallery\"];\n\n async function list(opts?: { isActive?: boolean }): Promise<GalleryCategoryItem[]> {\n const where =\n opts?.isActive !== undefined ? { isActive: opts.isActive } : undefined;\n const rows = await categoryDelegate().findMany({\n ...(where ? { where } : {}),\n orderBy: [{ sortOrder: \"asc\" }, { createdAt: \"asc\" }],\n });\n return rows as GalleryCategoryItem[];\n }\n\n async function getBySlug(slug: string): Promise<GalleryCategoryItem | null> {\n const row = await categoryDelegate().findUnique({ where: { slug } });\n return (row as GalleryCategoryItem | null) ?? null;\n }\n\n async function getById(id: string): Promise<GalleryCategoryItem | null> {\n const row = await categoryDelegate().findUnique({ where: { id } });\n return (row as GalleryCategoryItem | null) ?? null;\n }\n\n async function create(data: CreateCategoryInput): Promise<GalleryCategoryItem> {\n const row = await categoryDelegate().create({ data });\n return row as GalleryCategoryItem;\n }\n\n async function update(\n id: string,\n data: UpdateCategoryInput,\n ): Promise<GalleryCategoryItem> {\n const row = await categoryDelegate().update({ where: { id }, data });\n return row as GalleryCategoryItem;\n }\n\n /**\n * 사용 중 카테고리 (galleries 가 참조) 는 삭제 차단.\n * DB 의 `onDelete: Restrict` 가 최종 방어선이지만, 친화적 에러를 위해 사전 count.\n */\n async function remove(id: string): Promise<void> {\n const inUse = await galleryDelegate().count({ where: { categoryId: id } });\n if (inUse > 0) {\n throw new CategoryInUseError(id, inUse);\n }\n await categoryDelegate().delete({ where: { id } });\n }\n\n /**\n * ids 배열 순서대로 sortOrder = 0..N-1 로 갱신.\n * 단일 mutation 묶음 — $transaction 사용 (배열 형태).\n */\n async function reorder(ids: string[]): Promise<void> {\n const ops = ids.map((id, idx) =>\n categoryDelegate().update({ where: { id }, data: { sortOrder: idx } }),\n );\n await config.prisma.$transaction(ops);\n }\n\n return { list, getBySlug, getById, create, update, remove, reorder };\n}\n\nexport type CategoryService = ReturnType<typeof createCategoryService>;\n","import type {\n GalleryConfig,\n GalleryDetail,\n GalleryListItem,\n CreateGalleryInput,\n UpdateGalleryInput,\n PaginatedResult,\n} from \"../types\";\nimport { GalleryNotFoundError } from \"../errors\";\nimport { buildPaginatedResult } from \"./helpers\";\n\nexport { buildPaginatedResult } from \"./helpers\";\nexport { createCategoryService } from \"./category-service\";\nexport type { CategoryService } from \"./category-service\";\n\ntype SortKey = \"sortOrder\" | \"createdAt\" | \"updatedAt\" | \"caption\";\n\n/**\n * spec §9-2 의 createGalleryService.\n *\n * 핵심 규칙:\n * - Prisma delegate 는 `config.prisma[config.modelName ?? \"gallery\"]` 패턴.\n * - `include: { category: true }` 만 사용. **author include 금지** (Gallery 모델에 author 관계 없음, §3 호스트 독립 원칙).\n * - 삭제 시 `config.storage` 가 isEnabled() true 면 collectKeys → deleteKeys.\n * - `revalidate` 는 service 에서 호출하지 않음 (route handler 책임).\n */\nexport function createGalleryService(config: GalleryConfig) {\n const galleryDelegate = () => config.prisma[config.modelName ?? \"gallery\"];\n const baseInclude = { category: true } as const;\n\n function orderByFor(sortBy: SortKey) {\n switch (sortBy) {\n case \"sortOrder\":\n return [{ sortOrder: \"asc\" as const }, { createdAt: \"desc\" as const }];\n case \"caption\":\n return { caption: \"asc\" as const };\n case \"createdAt\":\n case \"updatedAt\":\n return { [sortBy]: \"desc\" as const };\n }\n }\n\n // ── Public-facing read methods ─────────────────────\n\n async function listFeatured(limit?: number): Promise<GalleryListItem[]> {\n const rows = await galleryDelegate().findMany({\n where: { published: true, featured: true },\n include: baseInclude,\n orderBy: [{ sortOrder: \"asc\" }, { createdAt: \"desc\" }],\n ...(limit ? { take: limit } : {}),\n });\n return rows as GalleryListItem[];\n }\n\n async function listPublished(limit?: number): Promise<GalleryListItem[]> {\n const rows = await galleryDelegate().findMany({\n where: { published: true },\n include: baseInclude,\n orderBy: [{ sortOrder: \"asc\" }, { createdAt: \"desc\" }],\n ...(limit ? { take: limit } : {}),\n });\n return rows as GalleryListItem[];\n }\n\n async function listPublishedByCategory(\n categorySlug: string,\n limit?: number,\n ): Promise<GalleryListItem[]> {\n const rows = await galleryDelegate().findMany({\n where: { published: true, category: { slug: categorySlug } },\n include: baseInclude,\n orderBy: [{ sortOrder: \"asc\" }, { createdAt: \"desc\" }],\n ...(limit ? { take: limit } : {}),\n });\n return rows as GalleryListItem[];\n }\n\n // ── Admin list / detail ────────────────────────────\n\n async function listAll(opts: {\n page?: number;\n limit?: number;\n categoryId?: string;\n published?: boolean;\n search?: string;\n sortBy?: SortKey;\n }): Promise<PaginatedResult<GalleryListItem>> {\n const page = opts.page ?? 1;\n const limit = opts.limit ?? 20;\n const sortBy = opts.sortBy ?? \"sortOrder\";\n const skip = (page - 1) * limit;\n\n const where: Record<string, unknown> = {};\n if (opts.categoryId) where.categoryId = opts.categoryId;\n if (opts.published !== undefined) where.published = opts.published;\n if (opts.search) where.caption = { contains: opts.search, mode: \"insensitive\" };\n\n const [items, total] = await Promise.all([\n galleryDelegate().findMany({\n where,\n include: baseInclude,\n orderBy: orderByFor(sortBy),\n skip,\n take: limit,\n }),\n galleryDelegate().count({ where }),\n ]);\n\n return buildPaginatedResult(items as GalleryListItem[], page, limit, total);\n }\n\n async function getById(id: string): Promise<GalleryDetail | null> {\n const row = await galleryDelegate().findUnique({\n where: { id },\n include: baseInclude,\n });\n return (row as GalleryDetail | null) ?? null;\n }\n\n // ── Mutations ──────────────────────────────────────\n\n async function create(\n data: CreateGalleryInput,\n authorId: string,\n ): Promise<GalleryDetail> {\n const row = await galleryDelegate().create({\n data: {\n imageUrl: data.imageUrl,\n imageKey: data.imageKey ?? null,\n caption: data.caption ?? null,\n categoryId: data.categoryId,\n sortOrder: data.sortOrder ?? 0,\n featured: data.featured ?? false,\n published: data.published ?? false,\n authorId,\n },\n include: baseInclude,\n });\n return row as GalleryDetail;\n }\n\n async function createMany(\n items: CreateGalleryInput[],\n authorId: string,\n ): Promise<{ count: number }> {\n const result = await galleryDelegate().createMany({\n data: items.map((it) => ({\n imageUrl: it.imageUrl,\n imageKey: it.imageKey ?? null,\n caption: it.caption ?? null,\n categoryId: it.categoryId,\n sortOrder: it.sortOrder ?? 0,\n featured: it.featured ?? false,\n published: it.published ?? false,\n authorId,\n })),\n });\n return { count: result.count };\n }\n\n async function update(\n id: string,\n data: UpdateGalleryInput,\n ): Promise<GalleryDetail> {\n const patch: Record<string, unknown> = {};\n if (data.imageUrl !== undefined) patch.imageUrl = data.imageUrl;\n if (data.imageKey !== undefined) patch.imageKey = data.imageKey ?? null;\n if (data.caption !== undefined) patch.caption = data.caption ?? null;\n if (data.categoryId !== undefined) patch.categoryId = data.categoryId;\n if (data.sortOrder !== undefined) patch.sortOrder = data.sortOrder;\n if (data.featured !== undefined) patch.featured = data.featured;\n if (data.published !== undefined) patch.published = data.published;\n\n const row = await galleryDelegate().update({\n where: { id },\n data: patch,\n include: baseInclude,\n });\n return row as GalleryDetail;\n }\n\n async function remove(id: string): Promise<void> {\n const storageEnabled = config.storage?.isEnabled?.() === true;\n let imageKey: string | null = null;\n if (storageEnabled) {\n const row = (await galleryDelegate().findUnique({\n where: { id },\n select: { imageKey: true },\n })) as { imageKey: string | null } | null;\n imageKey = row?.imageKey ?? null;\n }\n await galleryDelegate().delete({ where: { id } });\n if (storageEnabled && imageKey && config.storage) {\n const keys = config.storage.collectKeys(imageKey);\n if (keys.length > 0) await config.storage.deleteKeys(keys);\n }\n }\n\n async function removeMany(ids: string[]): Promise<{ count: number }> {\n const storageEnabled = config.storage?.isEnabled?.() === true;\n let imageKeys: string[] = [];\n if (storageEnabled) {\n const rows = (await galleryDelegate().findMany({\n where: { id: { in: ids } },\n select: { imageKey: true },\n })) as Array<{ imageKey: string | null }>;\n imageKeys = rows\n .map((r) => r.imageKey)\n .filter((k): k is string => typeof k === \"string\" && k.length > 0);\n }\n const result = await galleryDelegate().deleteMany({ where: { id: { in: ids } } });\n if (storageEnabled && imageKeys.length > 0 && config.storage) {\n const allKeys = imageKeys.flatMap((k) => config.storage!.collectKeys(k));\n const uniqueKeys = [...new Set(allKeys)];\n if (uniqueKeys.length > 0) await config.storage.deleteKeys(uniqueKeys);\n }\n return { count: result.count };\n }\n\n async function bulkUpdatePublished(\n ids: string[],\n published: boolean,\n ): Promise<{ count: number }> {\n const result = await galleryDelegate().updateMany({\n where: { id: { in: ids } },\n data: { published, updatedAt: new Date() },\n });\n return { count: result.count };\n }\n\n async function bulkUpdateFeatured(\n ids: string[],\n featured: boolean,\n ): Promise<{ count: number }> {\n const result = await galleryDelegate().updateMany({\n where: { id: { in: ids } },\n data: { featured, updatedAt: new Date() },\n });\n return { count: result.count };\n }\n\n async function togglePublish(id: string): Promise<GalleryDetail> {\n const current = (await galleryDelegate().findUnique({\n where: { id },\n select: { published: true },\n })) as { published: boolean } | null;\n if (!current) {\n throw new GalleryNotFoundError(id);\n }\n const row = await galleryDelegate().update({\n where: { id },\n data: { published: !current.published },\n include: baseInclude,\n });\n return row as GalleryDetail;\n }\n\n async function count(opts?: { published?: boolean }): Promise<number> {\n const where = opts?.published !== undefined ? { published: opts.published } : undefined;\n const n = await galleryDelegate().count(where ? { where } : {});\n return n as number;\n }\n\n async function listRecent(limit: number): Promise<GalleryListItem[]> {\n const rows = await galleryDelegate().findMany({\n include: baseInclude,\n orderBy: { createdAt: \"desc\" },\n take: limit,\n });\n return rows as GalleryListItem[];\n }\n\n return {\n listFeatured,\n listPublished,\n listPublishedByCategory,\n listAll,\n getById,\n create,\n createMany,\n update,\n remove,\n removeMany,\n bulkUpdatePublished,\n bulkUpdateFeatured,\n togglePublish,\n count,\n listRecent,\n };\n}\n\nexport type GalleryService = ReturnType<typeof createGalleryService>;\n","/**\n * Route handler factory — spec §9-3.\n *\n * `createGalleryRoutes(config)` 가 host 의 `apiWrapper` 로 감싼 9 개 라우트를 반환한다.\n * host 는 각 라우트 파일에서 `export const { GET, POST } = galleryRoutes.collection` 식으로 사용.\n *\n * 설계 원칙:\n * - 모든 mutation 종료 직전 `config.revalidate?.(path)` 를 `config.revalidatePaths` 각 path 에 대해 호출.\n * - `config.permissions?.canEdit/canDelete` 가 함수면 호출 → false 시 403 반환.\n * 미설정이면 admin 전원 모든 권한 (spec §16 v0.1 정책).\n * - `ctx.params?.id` 는 host 미들웨어가 채워준다고 가정 (spec §6-1).\n * - 카테고리 `remove` 의 in-use 에러는 service throw → handler 가 잡아 409 반환.\n */\nimport { NextResponse } from \"next/server\";\nimport type {\n ApiContext,\n GalleryConfig,\n RouteHandler,\n} from \"../types\";\nimport { createGalleryService } from \"../services\";\nimport { createCategoryService } from \"../services/category-service\";\nimport { createGallerySchemas } from \"../validators\";\nimport {\n CategoryInUseError,\n GalleryNotFoundError,\n} from \"../errors\";\nimport {\n parsePagination,\n getSearchParam,\n parseSortKey,\n validateAndParse,\n validateIds,\n} from \"../utils/api-helpers\";\n\nconst VALID_SORT_KEYS = [\n \"sortOrder\",\n \"createdAt\",\n \"updatedAt\",\n \"caption\",\n] as const;\n\ntype SortKey = (typeof VALID_SORT_KEYS)[number];\n\nexport interface GalleryRoutes {\n collection: { GET: RouteHandler; POST: RouteHandler; DELETE: RouteHandler };\n item: { GET: RouteHandler; PUT: RouteHandler; DELETE: RouteHandler };\n publishToggle: { PATCH: RouteHandler };\n bulk: { POST: RouteHandler; PATCH: RouteHandler };\n categoryCollection: { GET: RouteHandler; POST: RouteHandler };\n categoryItem: {\n GET: RouteHandler;\n PUT: RouteHandler;\n DELETE: RouteHandler;\n };\n}\n\nfunction jsonError(message: string, status: number, error?: string): Response {\n return NextResponse.json(\n { success: false, error: error ?? \"Error\", message },\n { status },\n );\n}\n\nfunction callRevalidate(config: GalleryConfig): void {\n if (!config.revalidate || !config.revalidatePaths) return;\n for (const p of config.revalidatePaths) {\n config.revalidate(p);\n }\n}\n\nfunction getAuthorId(config: GalleryConfig, ctx: ApiContext): string | null {\n if (config.authorIdFromContext) {\n try {\n const id = config.authorIdFromContext(ctx);\n return id || null;\n } catch {\n return null;\n }\n }\n return ctx.user?.id ?? null;\n}\n\nfunction getRouteId(ctx: ApiContext): string | null {\n const id = ctx.params?.id;\n return typeof id === \"string\" && id.length > 0 ? id : null;\n}\n\nexport function createGalleryRoutes(config: GalleryConfig): GalleryRoutes {\n const service = createGalleryService(config);\n const categoryService = createCategoryService(config);\n const schemas = createGallerySchemas({\n batchMax: config.limits.batchMax,\n captionMaxLength: config.limits.captionMaxLength ?? 200,\n });\n\n // ── /api/admin/galleries ─────────────────────────────\n\n const collectionGET = config.apiWrapper(async (ctx: ApiContext) => {\n const { page, limit } = parsePagination(ctx.request);\n const { searchParams } = new URL(ctx.request.url);\n const categoryId = getSearchParam(ctx.request, \"categoryId\");\n const publishedRaw = getSearchParam(ctx.request, \"published\");\n const search = getSearchParam(ctx.request, \"search\");\n const sortBy = parseSortKey<SortKey>(\n searchParams,\n VALID_SORT_KEYS,\n \"sortOrder\",\n );\n\n const published =\n publishedRaw === \"true\"\n ? true\n : publishedRaw === \"false\"\n ? false\n : undefined;\n\n const result = await service.listAll({\n page,\n limit,\n categoryId,\n published,\n search,\n sortBy,\n });\n\n return NextResponse.json({ success: true, data: result });\n });\n\n const collectionPOST = config.apiWrapper(async (ctx: ApiContext) => {\n const authorId = getAuthorId(config, ctx);\n if (!authorId) return jsonError(\"authentication required\", 401, \"Unauthorized\");\n\n const body = await ctx.request.json();\n const validation = validateAndParse(schemas.CreateGallerySchema, body);\n if (!validation.success) return validation.response;\n\n const item = await service.create(validation.data, authorId);\n callRevalidate(config);\n return NextResponse.json({ success: true, data: item }, { status: 201 });\n });\n\n const collectionDELETE = config.apiWrapper(async (ctx: ApiContext) => {\n const body = await ctx.request.json();\n const check = validateIds(body?.ids);\n if (!check.valid) return check.response;\n\n const { count } = await service.removeMany(check.ids);\n callRevalidate(config);\n return NextResponse.json({ success: true, data: { count } });\n });\n\n // ── /api/admin/galleries/[id] ────────────────────────\n\n const itemGET = config.apiWrapper(async (ctx: ApiContext) => {\n const id = getRouteId(ctx);\n if (!id) return jsonError(\"missing route param: id\", 400, \"InvalidParams\");\n\n const item = await service.getById(id);\n if (!item) return jsonError(\"Gallery item not found\", 404, \"NotFound\");\n\n return NextResponse.json({ success: true, data: item });\n });\n\n const itemPUT = config.apiWrapper(async (ctx: ApiContext) => {\n const id = getRouteId(ctx);\n if (!id) return jsonError(\"missing route param: id\", 400, \"InvalidParams\");\n\n const body = await ctx.request.json();\n const validation = validateAndParse(schemas.UpdateGallerySchema, body);\n if (!validation.success) return validation.response;\n\n const existing = await service.getById(id);\n if (!existing) return jsonError(\"Gallery item not found\", 404, \"NotFound\");\n\n if (config.permissions?.canEdit) {\n const ok = config.permissions.canEdit(ctx, { authorId: existing.authorId });\n if (!ok) return jsonError(\"forbidden\", 403, \"Forbidden\");\n }\n\n const item = await service.update(id, validation.data);\n callRevalidate(config);\n return NextResponse.json({ success: true, data: item });\n });\n\n const itemDELETE = config.apiWrapper(async (ctx: ApiContext) => {\n const id = getRouteId(ctx);\n if (!id) return jsonError(\"missing route param: id\", 400, \"InvalidParams\");\n\n if (config.permissions?.canDelete) {\n const existing = await service.getById(id);\n if (!existing) return jsonError(\"Gallery item not found\", 404, \"NotFound\");\n const ok = config.permissions.canDelete(ctx, {\n authorId: existing.authorId,\n });\n if (!ok) return jsonError(\"forbidden\", 403, \"Forbidden\");\n }\n\n await service.remove(id);\n callRevalidate(config);\n return new NextResponse(null, { status: 204 });\n });\n\n // ── /api/admin/galleries/[id]/publish ────────────────\n\n const publishTogglePATCH = config.apiWrapper(async (ctx: ApiContext) => {\n const id = getRouteId(ctx);\n if (!id) return jsonError(\"missing route param: id\", 400, \"InvalidParams\");\n\n if (config.permissions?.canEdit) {\n const existing = await service.getById(id);\n if (!existing) return jsonError(\"Gallery item not found\", 404, \"NotFound\");\n const ok = config.permissions.canEdit(ctx, {\n authorId: existing.authorId,\n });\n if (!ok) return jsonError(\"forbidden\", 403, \"Forbidden\");\n }\n\n try {\n const result = await service.togglePublish(id);\n callRevalidate(config);\n return NextResponse.json({ success: true, data: result });\n } catch (err) {\n // service.togglePublish throws GalleryNotFoundError on missing id — map to 404\n if (err instanceof GalleryNotFoundError) {\n return jsonError(\"Gallery item not found\", 404, \"NotFound\");\n }\n throw err;\n }\n });\n\n // ── /api/admin/galleries/bulk ────────────────────────\n\n const bulkPOST = config.apiWrapper(async (ctx: ApiContext) => {\n const authorId = getAuthorId(config, ctx);\n if (!authorId) return jsonError(\"authentication required\", 401, \"Unauthorized\");\n\n const body = await ctx.request.json();\n const validation = validateAndParse(schemas.BatchCreateGallerySchema, body);\n if (!validation.success) return validation.response;\n\n const result = await service.createMany(validation.data.items, authorId);\n callRevalidate(config);\n return NextResponse.json(\n { success: true, data: result },\n { status: 201 },\n );\n });\n\n const bulkPATCH = config.apiWrapper(async (ctx: ApiContext) => {\n const body = await ctx.request.json();\n const validation = validateAndParse(schemas.BulkUpdateSchema, body);\n if (!validation.success) return validation.response;\n\n const { ids, published, featured } = validation.data;\n let count = 0;\n\n if (published !== undefined) {\n const r = await service.bulkUpdatePublished(ids, published);\n count = r.count;\n }\n if (featured !== undefined) {\n const r = await service.bulkUpdateFeatured(ids, featured);\n count = r.count;\n }\n\n callRevalidate(config);\n return NextResponse.json({ success: true, data: { count } });\n });\n\n // ── /api/admin/gallery-categories ────────────────────\n\n const categoryCollectionGET = config.apiWrapper(async (ctx: ApiContext) => {\n const isActiveRaw = getSearchParam(ctx.request, \"isActive\");\n const isActive =\n isActiveRaw === \"true\"\n ? true\n : isActiveRaw === \"false\"\n ? false\n : undefined;\n\n const items = await categoryService.list(\n isActive === undefined ? undefined : { isActive },\n );\n return NextResponse.json({ success: true, data: items });\n });\n\n const categoryCollectionPOST = config.apiWrapper(async (ctx: ApiContext) => {\n const body = await ctx.request.json();\n const validation = validateAndParse(schemas.CreateCategorySchema, body);\n if (!validation.success) return validation.response;\n\n const item = await categoryService.create(validation.data);\n callRevalidate(config);\n return NextResponse.json({ success: true, data: item }, { status: 201 });\n });\n\n // ── /api/admin/gallery-categories/[id] ───────────────\n\n const categoryItemGET = config.apiWrapper(async (ctx: ApiContext) => {\n const id = getRouteId(ctx);\n if (!id) return jsonError(\"missing route param: id\", 400, \"InvalidParams\");\n\n const item = await categoryService.getById(id);\n if (!item) return jsonError(\"Category not found\", 404, \"NotFound\");\n return NextResponse.json({ success: true, data: item });\n });\n\n const categoryItemPUT = config.apiWrapper(async (ctx: ApiContext) => {\n const id = getRouteId(ctx);\n if (!id) return jsonError(\"missing route param: id\", 400, \"InvalidParams\");\n\n const body = await ctx.request.json();\n const validation = validateAndParse(schemas.UpdateCategorySchema, body);\n if (!validation.success) return validation.response;\n\n const existing = await categoryService.getById(id);\n if (!existing) return jsonError(\"Category not found\", 404, \"NotFound\");\n\n const item = await categoryService.update(id, validation.data);\n callRevalidate(config);\n return NextResponse.json({ success: true, data: item });\n });\n\n const categoryItemDELETE = config.apiWrapper(async (ctx: ApiContext) => {\n const id = getRouteId(ctx);\n if (!id) return jsonError(\"missing route param: id\", 400, \"InvalidParams\");\n\n try {\n await categoryService.remove(id);\n } catch (err) {\n if (err instanceof CategoryInUseError) {\n return jsonError(err.message, 409, \"CategoryInUse\");\n }\n throw err;\n }\n callRevalidate(config);\n return new NextResponse(null, { status: 204 });\n });\n\n return {\n collection: {\n GET: collectionGET,\n POST: collectionPOST,\n DELETE: collectionDELETE,\n },\n item: { GET: itemGET, PUT: itemPUT, DELETE: itemDELETE },\n publishToggle: { PATCH: publishTogglePATCH },\n bulk: { POST: bulkPOST, PATCH: bulkPATCH },\n categoryCollection: {\n GET: categoryCollectionGET,\n POST: categoryCollectionPOST,\n },\n categoryItem: {\n GET: categoryItemGET,\n PUT: categoryItemPUT,\n DELETE: categoryItemDELETE,\n },\n };\n}\n","import { NextResponse } from \"next/server\";\nimport type { z } from \"zod\";\n\ninterface RequestLike {\n url: string;\n}\n\nexport function parsePagination(req: RequestLike): { page: number; limit: number } {\n const { searchParams } = new URL(req.url);\n const rawPage = Number.parseInt(searchParams.get(\"page\") ?? \"1\", 10);\n const rawLimit = Number.parseInt(searchParams.get(\"limit\") ?? \"20\", 10);\n const page = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 1;\n const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, 100) : 20;\n return { page, limit };\n}\n\nexport function getSearchParam(req: RequestLike, key: string): string | undefined {\n const { searchParams } = new URL(req.url);\n const v = searchParams.get(key);\n return v ?? undefined;\n}\n\nexport type ValidateAndParseResult<T> =\n | { success: true; data: T }\n | { success: false; response: Response };\n\nexport function validateAndParse<T>(\n schema: z.ZodSchema<T>,\n data: unknown,\n): ValidateAndParseResult<T> {\n const parsed = schema.safeParse(data);\n if (parsed.success) return { success: true, data: parsed.data };\n return {\n success: false,\n response: NextResponse.json(\n { success: false, error: \"ValidationError\", details: parsed.error.issues },\n { status: 400 },\n ),\n };\n}\n\nexport type ValidateIdsResult =\n | { valid: true; ids: string[] }\n | { valid: false; response: Response };\n\nexport function validateIds(input: unknown): ValidateIdsResult {\n if (!Array.isArray(input) || input.length === 0) {\n return {\n valid: false,\n response: NextResponse.json(\n { success: false, error: \"InvalidIds\", message: \"ids must be a non-empty array\" },\n { status: 400 },\n ),\n };\n }\n const ids = input.filter((x): x is string => typeof x === \"string\" && x.length > 0);\n if (ids.length === 0) {\n return {\n valid: false,\n response: NextResponse.json(\n { success: false, error: \"InvalidIds\", message: \"ids array contains no valid strings\" },\n { status: 400 },\n ),\n };\n }\n return { valid: true, ids };\n}\n\nexport function parseSortKey<T extends string>(\n searchParams: URLSearchParams,\n allowed: readonly T[],\n fallback: T,\n): T {\n const raw = searchParams.get(\"sortBy\");\n return raw && (allowed as readonly string[]).includes(raw) ? (raw as T) : fallback;\n}\n","/**\n * RSC loaders — spec §9-4.\n *\n * Server Components 가 직접 호출하는 함수. 각 함수는 service 의 대응 메서드를\n * 얇게 감싸기만 한다. host 가 매번 `config` 를 주입해 explicit DI 를 유지한다\n * (setGalleryConfig 의존 회피 — host 의 layout/page 에서 config 를 명시적으로\n * 주입하는 것이 RSC graph 캐싱과 호환성 면에서 더 안전).\n */\nimport type {\n GalleryConfig,\n GalleryListItem,\n PaginatedResult,\n} from \"../types\";\nimport { createGalleryService } from \"../services\";\n\ntype SortKey = \"sortOrder\" | \"createdAt\" | \"updatedAt\" | \"caption\";\n\n/**\n * spec §9-4: `getGalleryItems(config, opts) → PaginatedResult<GalleryListItem>`\n *\n * admin 페이지의 RSC 가 직접 호출. fetch 라운드트립 없이 service 호출.\n */\nexport async function getGalleryItems(\n config: GalleryConfig,\n opts: {\n page?: number;\n limit?: number;\n categoryId?: string;\n published?: boolean;\n search?: string;\n sortBy?: SortKey;\n },\n): Promise<PaginatedResult<GalleryListItem>> {\n const service = createGalleryService(config);\n return service.listAll(opts);\n}\n\n/**\n * spec §9-4: `getFeaturedGalleries(config, limit?) → GalleryListItem[]`\n *\n * 홈 페이지의 모자이크 그리드 등 featured 영역에서 호출.\n */\nexport async function getFeaturedGalleries(\n config: GalleryConfig,\n limit?: number,\n): Promise<GalleryListItem[]> {\n const service = createGalleryService(config);\n return service.listFeatured(limit);\n}\n\n/**\n * spec §9-4: `getRecentGalleries(config, limit) → GalleryListItem[]`\n *\n * 대시보드의 최근 항목 위젯 등에서 호출.\n */\nexport async function getRecentGalleries(\n config: GalleryConfig,\n limit: number,\n): Promise<GalleryListItem[]> {\n const service = createGalleryService(config);\n return service.listRecent(limit);\n}\n\n/**\n * spec §9-4: `getGalleryCount(config, opts?) → number`\n *\n * 대시보드 통계 등에서 호출. `opts.published` 미지정 시 전체 count.\n */\nexport async function getGalleryCount(\n config: GalleryConfig,\n opts?: { published?: boolean },\n): Promise<number> {\n const service = createGalleryService(config);\n return service.count(opts);\n}\n"],"mappings":"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","names":["NextResponse","NextResponse"]}
|
|
1
|
+
{"version":3,"sources":["../../src/services/helpers.ts","../../src/services/category-service.ts","../../src/services/index.ts","../../src/server/route-handlers.ts","../../src/utils/api-helpers.ts","../../src/server/loaders.ts"],"sourcesContent":["import type { PaginatedResult } from \"../types/pagination\";\n\n/**\n * pagination math 를 일관되게 처리. ballet 의 `buildPaginatedResult` 와 동일 시맨틱.\n *\n * - total === 0 일 때 totalPages = 0 (clamp 안 함; UI 에서 \"결과 없음\" 로 처리)\n * - page > totalPages 여도 결과 자체는 caller 의 query 가 결정 — meta 만 계산\n */\nexport function buildPaginatedResult<T>(\n items: T[],\n page: number,\n limit: number,\n total: number,\n): PaginatedResult<T> {\n const totalPages = total === 0 ? 0 : Math.ceil(total / limit);\n return {\n items,\n meta: {\n page,\n limit,\n total,\n totalPages,\n hasNext: page < totalPages,\n hasPrev: page > 1,\n },\n };\n}\n","import type {\n GalleryConfig,\n GalleryCategoryItem,\n CreateCategoryInput,\n UpdateCategoryInput,\n} from \"../types\";\nimport { CategoryInUseError } from \"../errors\";\n\n/**\n * spec §9-2 의 categoryService.\n *\n * 호스트 독립성을 위해 Prisma delegate 는 `config.prisma[modelName]` 패턴으로 접근한다\n * (modelName 기본값 = \"galleryCategory\", gallery 본체 modelName 기본값 = \"gallery\").\n *\n * remove() 는 `onDelete: Restrict` 보강 — 사전 gallery.count() 후 throw.\n * admin UX 가 친화적 에러 메시지를 표시하도록 명시적 throw 를 사용.\n */\nexport function createCategoryService(config: GalleryConfig) {\n const categoryDelegate = () =>\n config.prisma[config.categoryModelName ?? \"galleryCategory\"];\n const galleryDelegate = () => config.prisma[config.modelName ?? \"gallery\"];\n\n async function list(opts?: { isActive?: boolean }): Promise<GalleryCategoryItem[]> {\n const where =\n opts?.isActive !== undefined ? { isActive: opts.isActive } : undefined;\n const rows = await categoryDelegate().findMany({\n ...(where ? { where } : {}),\n orderBy: [{ sortOrder: \"asc\" }, { createdAt: \"asc\" }],\n });\n return rows as GalleryCategoryItem[];\n }\n\n async function getBySlug(slug: string): Promise<GalleryCategoryItem | null> {\n const row = await categoryDelegate().findUnique({ where: { slug } });\n return (row as GalleryCategoryItem | null) ?? null;\n }\n\n async function getById(id: string): Promise<GalleryCategoryItem | null> {\n const row = await categoryDelegate().findUnique({ where: { id } });\n return (row as GalleryCategoryItem | null) ?? null;\n }\n\n async function create(data: CreateCategoryInput): Promise<GalleryCategoryItem> {\n const row = await categoryDelegate().create({ data });\n return row as GalleryCategoryItem;\n }\n\n async function update(\n id: string,\n data: UpdateCategoryInput,\n ): Promise<GalleryCategoryItem> {\n const row = await categoryDelegate().update({ where: { id }, data });\n return row as GalleryCategoryItem;\n }\n\n /**\n * 사용 중 카테고리 (galleries 가 참조) 는 삭제 차단.\n * DB 의 `onDelete: Restrict` 가 최종 방어선이지만, 친화적 에러를 위해 사전 count.\n */\n async function remove(id: string): Promise<void> {\n const inUse = await galleryDelegate().count({ where: { categoryId: id } });\n if (inUse > 0) {\n throw new CategoryInUseError(id, inUse);\n }\n await categoryDelegate().delete({ where: { id } });\n }\n\n /**\n * ids 배열 순서대로 sortOrder = 0..N-1 로 갱신.\n * 단일 mutation 묶음 — $transaction 사용 (배열 형태).\n */\n async function reorder(ids: string[]): Promise<void> {\n const ops = ids.map((id, idx) =>\n categoryDelegate().update({ where: { id }, data: { sortOrder: idx } }),\n );\n await config.prisma.$transaction(ops);\n }\n\n return { list, getBySlug, getById, create, update, remove, reorder };\n}\n\nexport type CategoryService = ReturnType<typeof createCategoryService>;\n","import type {\n GalleryConfig,\n GalleryDetail,\n GalleryListItem,\n CreateGalleryInput,\n UpdateGalleryInput,\n PaginatedResult,\n} from \"../types\";\nimport { GalleryNotFoundError } from \"../errors\";\nimport { buildPaginatedResult } from \"./helpers\";\n\nexport { buildPaginatedResult } from \"./helpers\";\nexport { createCategoryService } from \"./category-service\";\nexport type { CategoryService } from \"./category-service\";\n\ntype SortKey = \"sortOrder\" | \"createdAt\" | \"updatedAt\" | \"caption\";\n\n/**\n * spec §9-2 의 createGalleryService.\n *\n * 핵심 규칙:\n * - Prisma delegate 는 `config.prisma[config.modelName ?? \"gallery\"]` 패턴.\n * - `include: { category: true }` 만 사용. **author include 금지** (Gallery 모델에 author 관계 없음, §3 호스트 독립 원칙).\n * - 삭제 시 `config.storage` 가 isEnabled() true 면 collectKeys → deleteKeys.\n * - `revalidate` 는 service 에서 호출하지 않음 (route handler 책임).\n */\nexport function createGalleryService(config: GalleryConfig) {\n const galleryDelegate = () => config.prisma[config.modelName ?? \"gallery\"];\n const baseInclude = { category: true } as const;\n\n function orderByFor(sortBy: SortKey) {\n switch (sortBy) {\n case \"sortOrder\":\n return [{ sortOrder: \"asc\" as const }, { createdAt: \"desc\" as const }];\n case \"caption\":\n return { caption: \"asc\" as const };\n case \"createdAt\":\n case \"updatedAt\":\n return { [sortBy]: \"desc\" as const };\n }\n }\n\n // ── Public-facing read methods ─────────────────────\n\n async function listFeatured(limit?: number): Promise<GalleryListItem[]> {\n const rows = await galleryDelegate().findMany({\n where: { published: true, featured: true },\n include: baseInclude,\n orderBy: [{ sortOrder: \"asc\" }, { createdAt: \"desc\" }],\n ...(limit ? { take: limit } : {}),\n });\n return rows as GalleryListItem[];\n }\n\n async function listPublished(limit?: number): Promise<GalleryListItem[]> {\n const rows = await galleryDelegate().findMany({\n where: { published: true },\n include: baseInclude,\n orderBy: [{ sortOrder: \"asc\" }, { createdAt: \"desc\" }],\n ...(limit ? { take: limit } : {}),\n });\n return rows as GalleryListItem[];\n }\n\n async function listPublishedByCategory(\n categorySlug: string,\n limit?: number,\n ): Promise<GalleryListItem[]> {\n const rows = await galleryDelegate().findMany({\n where: { published: true, category: { slug: categorySlug } },\n include: baseInclude,\n orderBy: [{ sortOrder: \"asc\" }, { createdAt: \"desc\" }],\n ...(limit ? { take: limit } : {}),\n });\n return rows as GalleryListItem[];\n }\n\n // ── Admin list / detail ────────────────────────────\n\n async function listAll(opts: {\n page?: number;\n limit?: number;\n categoryId?: string;\n published?: boolean;\n search?: string;\n sortBy?: SortKey;\n }): Promise<PaginatedResult<GalleryListItem>> {\n const page = opts.page ?? 1;\n const limit = opts.limit ?? 20;\n const sortBy = opts.sortBy ?? \"sortOrder\";\n const skip = (page - 1) * limit;\n\n const where: Record<string, unknown> = {};\n if (opts.categoryId) where.categoryId = opts.categoryId;\n if (opts.published !== undefined) where.published = opts.published;\n if (opts.search) where.caption = { contains: opts.search, mode: \"insensitive\" };\n\n const [items, total] = await Promise.all([\n galleryDelegate().findMany({\n where,\n include: baseInclude,\n orderBy: orderByFor(sortBy),\n skip,\n take: limit,\n }),\n galleryDelegate().count({ where }),\n ]);\n\n return buildPaginatedResult(items as GalleryListItem[], page, limit, total);\n }\n\n async function getById(id: string): Promise<GalleryDetail | null> {\n const row = await galleryDelegate().findUnique({\n where: { id },\n include: baseInclude,\n });\n return (row as GalleryDetail | null) ?? null;\n }\n\n /**\n * bulk 권한 검사용 — ids 에 해당하는 row 의 {id, authorId} 만 조회.\n * 존재하지 않는 id 는 결과에서 빠진다 (caller 가 길이 비교로 판단).\n */\n async function getAuthorIds(\n ids: string[],\n ): Promise<Array<{ id: string; authorId: string }>> {\n const rows = await galleryDelegate().findMany({\n where: { id: { in: ids } },\n select: { id: true, authorId: true },\n });\n return rows as Array<{ id: string; authorId: string }>;\n }\n\n // ── Mutations ──────────────────────────────────────\n\n async function create(\n data: CreateGalleryInput,\n authorId: string,\n ): Promise<GalleryDetail> {\n const row = await galleryDelegate().create({\n data: {\n imageUrl: data.imageUrl,\n imageKey: data.imageKey ?? null,\n caption: data.caption ?? null,\n categoryId: data.categoryId,\n sortOrder: data.sortOrder ?? 0,\n featured: data.featured ?? false,\n published: data.published ?? false,\n authorId,\n },\n include: baseInclude,\n });\n return row as GalleryDetail;\n }\n\n async function createMany(\n items: CreateGalleryInput[],\n authorId: string,\n ): Promise<{ count: number }> {\n const result = await galleryDelegate().createMany({\n data: items.map((it) => ({\n imageUrl: it.imageUrl,\n imageKey: it.imageKey ?? null,\n caption: it.caption ?? null,\n categoryId: it.categoryId,\n sortOrder: it.sortOrder ?? 0,\n featured: it.featured ?? false,\n published: it.published ?? false,\n authorId,\n })),\n });\n return { count: result.count };\n }\n\n async function update(\n id: string,\n data: UpdateGalleryInput,\n ): Promise<GalleryDetail> {\n const patch: Record<string, unknown> = {};\n if (data.imageUrl !== undefined) patch.imageUrl = data.imageUrl;\n if (data.imageKey !== undefined) patch.imageKey = data.imageKey ?? null;\n if (data.caption !== undefined) patch.caption = data.caption ?? null;\n if (data.categoryId !== undefined) patch.categoryId = data.categoryId;\n if (data.sortOrder !== undefined) patch.sortOrder = data.sortOrder;\n if (data.featured !== undefined) patch.featured = data.featured;\n if (data.published !== undefined) patch.published = data.published;\n\n const row = await galleryDelegate().update({\n where: { id },\n data: patch,\n include: baseInclude,\n });\n return row as GalleryDetail;\n }\n\n async function remove(id: string): Promise<void> {\n const storageEnabled = config.storage?.isEnabled?.() === true;\n let imageKey: string | null = null;\n if (storageEnabled) {\n const row = (await galleryDelegate().findUnique({\n where: { id },\n select: { imageKey: true },\n })) as { imageKey: string | null } | null;\n imageKey = row?.imageKey ?? null;\n }\n await galleryDelegate().delete({ where: { id } });\n if (storageEnabled && imageKey && config.storage) {\n const keys = config.storage.collectKeys(imageKey);\n if (keys.length > 0) await config.storage.deleteKeys(keys);\n }\n }\n\n async function removeMany(ids: string[]): Promise<{ count: number }> {\n const storageEnabled = config.storage?.isEnabled?.() === true;\n let imageKeys: string[] = [];\n if (storageEnabled) {\n const rows = (await galleryDelegate().findMany({\n where: { id: { in: ids } },\n select: { imageKey: true },\n })) as Array<{ imageKey: string | null }>;\n imageKeys = rows\n .map((r) => r.imageKey)\n .filter((k): k is string => typeof k === \"string\" && k.length > 0);\n }\n const result = await galleryDelegate().deleteMany({ where: { id: { in: ids } } });\n if (storageEnabled && imageKeys.length > 0 && config.storage) {\n const allKeys = imageKeys.flatMap((k) => config.storage!.collectKeys(k));\n const uniqueKeys = [...new Set(allKeys)];\n if (uniqueKeys.length > 0) await config.storage.deleteKeys(uniqueKeys);\n }\n return { count: result.count };\n }\n\n async function bulkUpdatePublished(\n ids: string[],\n published: boolean,\n ): Promise<{ count: number }> {\n const result = await galleryDelegate().updateMany({\n where: { id: { in: ids } },\n data: { published, updatedAt: new Date() },\n });\n return { count: result.count };\n }\n\n async function bulkUpdateFeatured(\n ids: string[],\n featured: boolean,\n ): Promise<{ count: number }> {\n const result = await galleryDelegate().updateMany({\n where: { id: { in: ids } },\n data: { featured, updatedAt: new Date() },\n });\n return { count: result.count };\n }\n\n async function togglePublish(id: string): Promise<GalleryDetail> {\n const current = (await galleryDelegate().findUnique({\n where: { id },\n select: { published: true },\n })) as { published: boolean } | null;\n if (!current) {\n throw new GalleryNotFoundError(id);\n }\n const row = await galleryDelegate().update({\n where: { id },\n data: { published: !current.published },\n include: baseInclude,\n });\n return row as GalleryDetail;\n }\n\n async function count(opts?: { published?: boolean }): Promise<number> {\n const where = opts?.published !== undefined ? { published: opts.published } : undefined;\n const n = await galleryDelegate().count(where ? { where } : {});\n return n as number;\n }\n\n async function listRecent(limit: number): Promise<GalleryListItem[]> {\n const rows = await galleryDelegate().findMany({\n include: baseInclude,\n orderBy: { createdAt: \"desc\" },\n take: limit,\n });\n return rows as GalleryListItem[];\n }\n\n return {\n listFeatured,\n listPublished,\n listPublishedByCategory,\n listAll,\n getById,\n getAuthorIds,\n create,\n createMany,\n update,\n remove,\n removeMany,\n bulkUpdatePublished,\n bulkUpdateFeatured,\n togglePublish,\n count,\n listRecent,\n };\n}\n\nexport type GalleryService = ReturnType<typeof createGalleryService>;\n","/**\n * Route handler factory — spec §9-3.\n *\n * `createGalleryRoutes(config)` 가 host 의 `apiWrapper` 로 감싼 9 개 라우트를 반환한다.\n * host 는 각 라우트 파일에서 `export const { GET, POST } = galleryRoutes.collection` 식으로 사용.\n *\n * 설계 원칙:\n * - 모든 mutation 종료 직전 `config.revalidate?.(path)` 를 `config.revalidatePaths` 각 path 에 대해 호출.\n * - `config.permissions?.canEdit/canDelete` 가 함수면 호출 → false 시 403 반환.\n * 미설정이면 admin 전원 모든 권한 (spec §16 v0.1 정책).\n * - bulk (collection DELETE / bulk PATCH) 도 동일 훅을 대상 전원에 적용한다.\n * 하나라도 거부되면 전체 403 (부분 성공 없음). 존재하지 않는 id 가 섞이면 404.\n * - `config.permissions?.canManageCategories` 가 함수면 카테고리 CUD 에 적용.\n * - `ctx.params?.id` 는 host 미들웨어가 채워준다고 가정 (spec §6-1).\n * - 카테고리 `remove` 의 in-use 에러는 service throw → handler 가 잡아 409 반환.\n */\nimport { NextResponse } from \"next/server\";\nimport type {\n ApiContext,\n GalleryConfig,\n RouteHandler,\n} from \"../types\";\nimport { createGalleryService } from \"../services\";\nimport { createCategoryService } from \"../services/category-service\";\nimport { createGallerySchemas } from \"../validators\";\nimport {\n CategoryInUseError,\n GalleryNotFoundError,\n} from \"../errors\";\nimport {\n parsePagination,\n getSearchParam,\n parseSortKey,\n validateAndParse,\n validateIds,\n} from \"../utils/api-helpers\";\n\nconst VALID_SORT_KEYS = [\n \"sortOrder\",\n \"createdAt\",\n \"updatedAt\",\n \"caption\",\n] as const;\n\ntype SortKey = (typeof VALID_SORT_KEYS)[number];\n\nexport interface GalleryRoutes {\n collection: { GET: RouteHandler; POST: RouteHandler; DELETE: RouteHandler };\n item: { GET: RouteHandler; PUT: RouteHandler; DELETE: RouteHandler };\n publishToggle: { PATCH: RouteHandler };\n bulk: { POST: RouteHandler; PATCH: RouteHandler };\n categoryCollection: { GET: RouteHandler; POST: RouteHandler };\n categoryItem: {\n GET: RouteHandler;\n PUT: RouteHandler;\n DELETE: RouteHandler;\n };\n}\n\nfunction jsonError(message: string, status: number, error?: string): Response {\n return NextResponse.json(\n { success: false, error: error ?? \"Error\", message },\n { status },\n );\n}\n\nfunction callRevalidate(config: GalleryConfig): void {\n if (!config.revalidate || !config.revalidatePaths) return;\n for (const p of config.revalidatePaths) {\n config.revalidate(p);\n }\n}\n\nfunction getAuthorId(config: GalleryConfig, ctx: ApiContext): string | null {\n if (config.authorIdFromContext) {\n try {\n const id = config.authorIdFromContext(ctx);\n return id || null;\n } catch {\n return null;\n }\n }\n return ctx.user?.id ?? null;\n}\n\nfunction getRouteId(ctx: ApiContext): string | null {\n const id = ctx.params?.id;\n return typeof id === \"string\" && id.length > 0 ? id : null;\n}\n\nconst DEFAULT_SEARCH_MAX_LENGTH = 100;\n\nfunction canManageCategories(config: GalleryConfig, ctx: ApiContext): boolean {\n const fn = config.permissions?.canManageCategories;\n return fn ? fn(ctx) === true : true;\n}\n\ntype BulkPermissionCheck =\n | { ok: true }\n | { ok: false; response: Response };\n\n/**\n * bulk 대상 전원에 대해 permission 훅을 적용한다.\n * - 훅 미설정: 통과.\n * - 대상 중 존재하지 않는 id: 404 (권한 검사 대상이 불명확하므로 거부).\n * - 하나라도 거부: 403.\n */\nasync function checkBulkPermission(\n ctx: ApiContext,\n ids: string[],\n hook: ((ctx: ApiContext, gallery: { authorId: string }) => boolean) | undefined,\n loadAuthorIds: (ids: string[]) => Promise<Array<{ id: string; authorId: string }>>,\n): Promise<BulkPermissionCheck> {\n if (!hook) return { ok: true };\n const unique = [...new Set(ids)];\n const rows = await loadAuthorIds(unique);\n if (rows.length !== unique.length) {\n return {\n ok: false,\n response: jsonError(\"one or more gallery items not found\", 404, \"NotFound\"),\n };\n }\n for (const row of rows) {\n if (!hook(ctx, { authorId: row.authorId })) {\n return { ok: false, response: jsonError(\"forbidden\", 403, \"Forbidden\") };\n }\n }\n return { ok: true };\n}\n\nexport function createGalleryRoutes(config: GalleryConfig): GalleryRoutes {\n const service = createGalleryService(config);\n const categoryService = createCategoryService(config);\n const schemas = createGallerySchemas({\n batchMax: config.limits.batchMax,\n captionMaxLength: config.limits.captionMaxLength ?? 200,\n imageUrlProtocols: config.validation?.imageUrlProtocols,\n imageUrlHosts: config.validation?.imageUrlHosts,\n imageKeyPattern: config.validation?.imageKeyPattern,\n });\n const searchMaxLength =\n config.validation?.searchMaxLength ?? DEFAULT_SEARCH_MAX_LENGTH;\n\n // ── /api/admin/galleries ─────────────────────────────\n\n const collectionGET = config.apiWrapper(async (ctx: ApiContext) => {\n const { page, limit } = parsePagination(ctx.request);\n const { searchParams } = new URL(ctx.request.url);\n const categoryId = getSearchParam(ctx.request, \"categoryId\");\n const publishedRaw = getSearchParam(ctx.request, \"published\");\n const searchRaw = getSearchParam(ctx.request, \"search\")?.trim();\n const search = searchRaw ? searchRaw.slice(0, searchMaxLength) : undefined;\n const sortBy = parseSortKey<SortKey>(\n searchParams,\n VALID_SORT_KEYS,\n \"sortOrder\",\n );\n\n const published =\n publishedRaw === \"true\"\n ? true\n : publishedRaw === \"false\"\n ? false\n : undefined;\n\n const result = await service.listAll({\n page,\n limit,\n categoryId,\n published,\n search,\n sortBy,\n });\n\n return NextResponse.json({ success: true, data: result });\n });\n\n const collectionPOST = config.apiWrapper(async (ctx: ApiContext) => {\n const authorId = getAuthorId(config, ctx);\n if (!authorId) return jsonError(\"authentication required\", 401, \"Unauthorized\");\n\n const body = await ctx.request.json();\n const validation = validateAndParse(schemas.CreateGallerySchema, body);\n if (!validation.success) return validation.response;\n\n const item = await service.create(validation.data, authorId);\n callRevalidate(config);\n return NextResponse.json({ success: true, data: item }, { status: 201 });\n });\n\n const collectionDELETE = config.apiWrapper(async (ctx: ApiContext) => {\n const body = await ctx.request.json();\n const check = validateIds(body?.ids);\n if (!check.valid) return check.response;\n if (check.ids.length > config.limits.batchMax) {\n return jsonError(\n `ids exceeds batchMax (${config.limits.batchMax})`,\n 400,\n \"InvalidIds\",\n );\n }\n\n const perm = await checkBulkPermission(\n ctx,\n check.ids,\n config.permissions?.canDelete,\n service.getAuthorIds,\n );\n if (!perm.ok) return perm.response;\n\n const { count } = await service.removeMany(check.ids);\n callRevalidate(config);\n return NextResponse.json({ success: true, data: { count } });\n });\n\n // ── /api/admin/galleries/[id] ────────────────────────\n\n const itemGET = config.apiWrapper(async (ctx: ApiContext) => {\n const id = getRouteId(ctx);\n if (!id) return jsonError(\"missing route param: id\", 400, \"InvalidParams\");\n\n const item = await service.getById(id);\n if (!item) return jsonError(\"Gallery item not found\", 404, \"NotFound\");\n\n return NextResponse.json({ success: true, data: item });\n });\n\n const itemPUT = config.apiWrapper(async (ctx: ApiContext) => {\n const id = getRouteId(ctx);\n if (!id) return jsonError(\"missing route param: id\", 400, \"InvalidParams\");\n\n const body = await ctx.request.json();\n const validation = validateAndParse(schemas.UpdateGallerySchema, body);\n if (!validation.success) return validation.response;\n\n const existing = await service.getById(id);\n if (!existing) return jsonError(\"Gallery item not found\", 404, \"NotFound\");\n\n if (config.permissions?.canEdit) {\n const ok = config.permissions.canEdit(ctx, { authorId: existing.authorId });\n if (!ok) return jsonError(\"forbidden\", 403, \"Forbidden\");\n }\n\n const item = await service.update(id, validation.data);\n callRevalidate(config);\n return NextResponse.json({ success: true, data: item });\n });\n\n const itemDELETE = config.apiWrapper(async (ctx: ApiContext) => {\n const id = getRouteId(ctx);\n if (!id) return jsonError(\"missing route param: id\", 400, \"InvalidParams\");\n\n if (config.permissions?.canDelete) {\n const existing = await service.getById(id);\n if (!existing) return jsonError(\"Gallery item not found\", 404, \"NotFound\");\n const ok = config.permissions.canDelete(ctx, {\n authorId: existing.authorId,\n });\n if (!ok) return jsonError(\"forbidden\", 403, \"Forbidden\");\n }\n\n await service.remove(id);\n callRevalidate(config);\n return new NextResponse(null, { status: 204 });\n });\n\n // ── /api/admin/galleries/[id]/publish ────────────────\n\n const publishTogglePATCH = config.apiWrapper(async (ctx: ApiContext) => {\n const id = getRouteId(ctx);\n if (!id) return jsonError(\"missing route param: id\", 400, \"InvalidParams\");\n\n if (config.permissions?.canEdit) {\n const existing = await service.getById(id);\n if (!existing) return jsonError(\"Gallery item not found\", 404, \"NotFound\");\n const ok = config.permissions.canEdit(ctx, {\n authorId: existing.authorId,\n });\n if (!ok) return jsonError(\"forbidden\", 403, \"Forbidden\");\n }\n\n try {\n const result = await service.togglePublish(id);\n callRevalidate(config);\n return NextResponse.json({ success: true, data: result });\n } catch (err) {\n // service.togglePublish throws GalleryNotFoundError on missing id — map to 404\n if (err instanceof GalleryNotFoundError) {\n return jsonError(\"Gallery item not found\", 404, \"NotFound\");\n }\n throw err;\n }\n });\n\n // ── /api/admin/galleries/bulk ────────────────────────\n\n const bulkPOST = config.apiWrapper(async (ctx: ApiContext) => {\n const authorId = getAuthorId(config, ctx);\n if (!authorId) return jsonError(\"authentication required\", 401, \"Unauthorized\");\n\n const body = await ctx.request.json();\n const validation = validateAndParse(schemas.BatchCreateGallerySchema, body);\n if (!validation.success) return validation.response;\n\n const result = await service.createMany(validation.data.items, authorId);\n callRevalidate(config);\n return NextResponse.json(\n { success: true, data: result },\n { status: 201 },\n );\n });\n\n const bulkPATCH = config.apiWrapper(async (ctx: ApiContext) => {\n const body = await ctx.request.json();\n const validation = validateAndParse(schemas.BulkUpdateSchema, body);\n if (!validation.success) return validation.response;\n\n const { ids, published, featured } = validation.data;\n\n const perm = await checkBulkPermission(\n ctx,\n ids,\n config.permissions?.canEdit,\n service.getAuthorIds,\n );\n if (!perm.ok) return perm.response;\n\n let count = 0;\n\n if (published !== undefined) {\n const r = await service.bulkUpdatePublished(ids, published);\n count = r.count;\n }\n if (featured !== undefined) {\n const r = await service.bulkUpdateFeatured(ids, featured);\n count = r.count;\n }\n\n callRevalidate(config);\n return NextResponse.json({ success: true, data: { count } });\n });\n\n // ── /api/admin/gallery-categories ────────────────────\n\n const categoryCollectionGET = config.apiWrapper(async (ctx: ApiContext) => {\n const isActiveRaw = getSearchParam(ctx.request, \"isActive\");\n const isActive =\n isActiveRaw === \"true\"\n ? true\n : isActiveRaw === \"false\"\n ? false\n : undefined;\n\n const items = await categoryService.list(\n isActive === undefined ? undefined : { isActive },\n );\n return NextResponse.json({ success: true, data: items });\n });\n\n const categoryCollectionPOST = config.apiWrapper(async (ctx: ApiContext) => {\n if (!canManageCategories(config, ctx)) return jsonError(\"forbidden\", 403, \"Forbidden\");\n\n const body = await ctx.request.json();\n const validation = validateAndParse(schemas.CreateCategorySchema, body);\n if (!validation.success) return validation.response;\n\n const item = await categoryService.create(validation.data);\n callRevalidate(config);\n return NextResponse.json({ success: true, data: item }, { status: 201 });\n });\n\n // ── /api/admin/gallery-categories/[id] ───────────────\n\n const categoryItemGET = config.apiWrapper(async (ctx: ApiContext) => {\n const id = getRouteId(ctx);\n if (!id) return jsonError(\"missing route param: id\", 400, \"InvalidParams\");\n\n const item = await categoryService.getById(id);\n if (!item) return jsonError(\"Category not found\", 404, \"NotFound\");\n return NextResponse.json({ success: true, data: item });\n });\n\n const categoryItemPUT = config.apiWrapper(async (ctx: ApiContext) => {\n const id = getRouteId(ctx);\n if (!id) return jsonError(\"missing route param: id\", 400, \"InvalidParams\");\n if (!canManageCategories(config, ctx)) return jsonError(\"forbidden\", 403, \"Forbidden\");\n\n const body = await ctx.request.json();\n const validation = validateAndParse(schemas.UpdateCategorySchema, body);\n if (!validation.success) return validation.response;\n\n const existing = await categoryService.getById(id);\n if (!existing) return jsonError(\"Category not found\", 404, \"NotFound\");\n\n const item = await categoryService.update(id, validation.data);\n callRevalidate(config);\n return NextResponse.json({ success: true, data: item });\n });\n\n const categoryItemDELETE = config.apiWrapper(async (ctx: ApiContext) => {\n const id = getRouteId(ctx);\n if (!id) return jsonError(\"missing route param: id\", 400, \"InvalidParams\");\n if (!canManageCategories(config, ctx)) return jsonError(\"forbidden\", 403, \"Forbidden\");\n\n try {\n await categoryService.remove(id);\n } catch (err) {\n if (err instanceof CategoryInUseError) {\n return jsonError(err.message, 409, \"CategoryInUse\");\n }\n throw err;\n }\n callRevalidate(config);\n return new NextResponse(null, { status: 204 });\n });\n\n return {\n collection: {\n GET: collectionGET,\n POST: collectionPOST,\n DELETE: collectionDELETE,\n },\n item: { GET: itemGET, PUT: itemPUT, DELETE: itemDELETE },\n publishToggle: { PATCH: publishTogglePATCH },\n bulk: { POST: bulkPOST, PATCH: bulkPATCH },\n categoryCollection: {\n GET: categoryCollectionGET,\n POST: categoryCollectionPOST,\n },\n categoryItem: {\n GET: categoryItemGET,\n PUT: categoryItemPUT,\n DELETE: categoryItemDELETE,\n },\n };\n}\n","import { NextResponse } from \"next/server\";\nimport type { z } from \"zod\";\n\ninterface RequestLike {\n url: string;\n}\n\nexport function parsePagination(req: RequestLike): { page: number; limit: number } {\n const { searchParams } = new URL(req.url);\n const rawPage = Number.parseInt(searchParams.get(\"page\") ?? \"1\", 10);\n const rawLimit = Number.parseInt(searchParams.get(\"limit\") ?? \"20\", 10);\n const page = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 1;\n const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, 100) : 20;\n return { page, limit };\n}\n\nexport function getSearchParam(req: RequestLike, key: string): string | undefined {\n const { searchParams } = new URL(req.url);\n const v = searchParams.get(key);\n return v ?? undefined;\n}\n\nexport type ValidateAndParseResult<T> =\n | { success: true; data: T }\n | { success: false; response: Response };\n\nexport function validateAndParse<T>(\n schema: z.ZodSchema<T>,\n data: unknown,\n): ValidateAndParseResult<T> {\n const parsed = schema.safeParse(data);\n if (parsed.success) return { success: true, data: parsed.data };\n return {\n success: false,\n response: NextResponse.json(\n { success: false, error: \"ValidationError\", details: parsed.error.issues },\n { status: 400 },\n ),\n };\n}\n\nexport type ValidateIdsResult =\n | { valid: true; ids: string[] }\n | { valid: false; response: Response };\n\nexport function validateIds(input: unknown): ValidateIdsResult {\n if (!Array.isArray(input) || input.length === 0) {\n return {\n valid: false,\n response: NextResponse.json(\n { success: false, error: \"InvalidIds\", message: \"ids must be a non-empty array\" },\n { status: 400 },\n ),\n };\n }\n const ids = input.filter((x): x is string => typeof x === \"string\" && x.length > 0);\n if (ids.length === 0) {\n return {\n valid: false,\n response: NextResponse.json(\n { success: false, error: \"InvalidIds\", message: \"ids array contains no valid strings\" },\n { status: 400 },\n ),\n };\n }\n return { valid: true, ids };\n}\n\nexport function parseSortKey<T extends string>(\n searchParams: URLSearchParams,\n allowed: readonly T[],\n fallback: T,\n): T {\n const raw = searchParams.get(\"sortBy\");\n return raw && (allowed as readonly string[]).includes(raw) ? (raw as T) : fallback;\n}\n","/**\n * RSC loaders — spec §9-4.\n *\n * Server Components 가 직접 호출하는 함수. 각 함수는 service 의 대응 메서드를\n * 얇게 감싸기만 한다. host 가 매번 `config` 를 주입해 explicit DI 를 유지한다\n * (setGalleryConfig 의존 회피 — host 의 layout/page 에서 config 를 명시적으로\n * 주입하는 것이 RSC graph 캐싱과 호환성 면에서 더 안전).\n */\nimport type {\n GalleryConfig,\n GalleryListItem,\n PaginatedResult,\n} from \"../types\";\nimport { createGalleryService } from \"../services\";\n\ntype SortKey = \"sortOrder\" | \"createdAt\" | \"updatedAt\" | \"caption\";\n\n/**\n * spec §9-4: `getGalleryItems(config, opts) → PaginatedResult<GalleryListItem>`\n *\n * admin 페이지의 RSC 가 직접 호출. fetch 라운드트립 없이 service 호출.\n */\nexport async function getGalleryItems(\n config: GalleryConfig,\n opts: {\n page?: number;\n limit?: number;\n categoryId?: string;\n published?: boolean;\n search?: string;\n sortBy?: SortKey;\n },\n): Promise<PaginatedResult<GalleryListItem>> {\n const service = createGalleryService(config);\n return service.listAll(opts);\n}\n\n/**\n * spec §9-4: `getFeaturedGalleries(config, limit?) → GalleryListItem[]`\n *\n * 홈 페이지의 모자이크 그리드 등 featured 영역에서 호출.\n */\nexport async function getFeaturedGalleries(\n config: GalleryConfig,\n limit?: number,\n): Promise<GalleryListItem[]> {\n const service = createGalleryService(config);\n return service.listFeatured(limit);\n}\n\n/**\n * spec §9-4: `getRecentGalleries(config, limit) → GalleryListItem[]`\n *\n * 대시보드의 최근 항목 위젯 등에서 호출.\n */\nexport async function getRecentGalleries(\n config: GalleryConfig,\n limit: number,\n): Promise<GalleryListItem[]> {\n const service = createGalleryService(config);\n return service.listRecent(limit);\n}\n\n/**\n * spec §9-4: `getGalleryCount(config, opts?) → number`\n *\n * 대시보드 통계 등에서 호출. `opts.published` 미지정 시 전체 count.\n */\nexport async function getGalleryCount(\n config: GalleryConfig,\n opts?: { published?: boolean },\n): Promise<number> {\n const service = createGalleryService(config);\n return service.count(opts);\n}\n"],"mappings":"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","names":["NextResponse","NextResponse"]}
|
package/dist/types/index.d.mts
CHANGED
|
@@ -97,12 +97,27 @@ interface GalleryConfig {
|
|
|
97
97
|
apiWrapper: ApiWrapper;
|
|
98
98
|
authorIdFromContext?: (ctx: ApiContext) => string;
|
|
99
99
|
permissions?: {
|
|
100
|
+
/** 단건 PUT / publish toggle / bulk PATCH 에 적용. bulk 는 대상 전원이 통과해야 한다. */
|
|
100
101
|
canEdit?: (ctx: ApiContext, gallery: {
|
|
101
102
|
authorId: string;
|
|
102
103
|
}) => boolean;
|
|
104
|
+
/** 단건 DELETE / collection DELETE(ids) 에 적용. bulk 는 대상 전원이 통과해야 한다. */
|
|
103
105
|
canDelete?: (ctx: ApiContext, gallery: {
|
|
104
106
|
authorId: string;
|
|
105
107
|
}) => boolean;
|
|
108
|
+
/** 카테고리 생성 / 수정 / 삭제 에 적용. 미설정 시 apiWrapper 통과자 전원 허용. */
|
|
109
|
+
canManageCategories?: (ctx: ApiContext) => boolean;
|
|
110
|
+
};
|
|
111
|
+
/** 입력 검증 강화 옵션. 미설정 시 안전한 기본값 사용. */
|
|
112
|
+
validation?: {
|
|
113
|
+
/** imageUrl 에 허용할 프로토콜. 기본 ["https:", "http:"] (javascript:/data: 차단). */
|
|
114
|
+
imageUrlProtocols?: string[];
|
|
115
|
+
/** imageUrl 에 허용할 호스트 allowlist. 미설정 시 모든 호스트 허용. */
|
|
116
|
+
imageUrlHosts?: string[];
|
|
117
|
+
/** imageKey 형식. 기본: 영숫자/._-/ 만 허용, 선행 슬래시·`..` 세그먼트 금지, 최대 512자. */
|
|
118
|
+
imageKeyPattern?: RegExp;
|
|
119
|
+
/** admin 목록 search 파라미터 최대 길이. 기본 100. */
|
|
120
|
+
searchMaxLength?: number;
|
|
106
121
|
};
|
|
107
122
|
storage?: {
|
|
108
123
|
isEnabled: () => boolean;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -97,12 +97,27 @@ interface GalleryConfig {
|
|
|
97
97
|
apiWrapper: ApiWrapper;
|
|
98
98
|
authorIdFromContext?: (ctx: ApiContext) => string;
|
|
99
99
|
permissions?: {
|
|
100
|
+
/** 단건 PUT / publish toggle / bulk PATCH 에 적용. bulk 는 대상 전원이 통과해야 한다. */
|
|
100
101
|
canEdit?: (ctx: ApiContext, gallery: {
|
|
101
102
|
authorId: string;
|
|
102
103
|
}) => boolean;
|
|
104
|
+
/** 단건 DELETE / collection DELETE(ids) 에 적용. bulk 는 대상 전원이 통과해야 한다. */
|
|
103
105
|
canDelete?: (ctx: ApiContext, gallery: {
|
|
104
106
|
authorId: string;
|
|
105
107
|
}) => boolean;
|
|
108
|
+
/** 카테고리 생성 / 수정 / 삭제 에 적용. 미설정 시 apiWrapper 통과자 전원 허용. */
|
|
109
|
+
canManageCategories?: (ctx: ApiContext) => boolean;
|
|
110
|
+
};
|
|
111
|
+
/** 입력 검증 강화 옵션. 미설정 시 안전한 기본값 사용. */
|
|
112
|
+
validation?: {
|
|
113
|
+
/** imageUrl 에 허용할 프로토콜. 기본 ["https:", "http:"] (javascript:/data: 차단). */
|
|
114
|
+
imageUrlProtocols?: string[];
|
|
115
|
+
/** imageUrl 에 허용할 호스트 allowlist. 미설정 시 모든 호스트 허용. */
|
|
116
|
+
imageUrlHosts?: string[];
|
|
117
|
+
/** imageKey 형식. 기본: 영숫자/._-/ 만 허용, 선행 슬래시·`..` 세그먼트 금지, 최대 512자. */
|
|
118
|
+
imageKeyPattern?: RegExp;
|
|
119
|
+
/** admin 목록 search 파라미터 최대 길이. 기본 100. */
|
|
120
|
+
searchMaxLength?: number;
|
|
106
121
|
};
|
|
107
122
|
storage?: {
|
|
108
123
|
isEnabled: () => boolean;
|
|
@@ -6,11 +6,25 @@ import { z } from 'zod';
|
|
|
6
6
|
* 호스트가 `config.limits.batchMax` 와 `config.limits.captionMaxLength` 를 넘겨
|
|
7
7
|
* 패키지 내부 스키마를 생성한다. categoryId 는 cuid 형식만 검증하고,
|
|
8
8
|
* 실제 카테고리 존재 여부는 service 레이어가 Prisma FK 제약으로 자연스럽게 잡는다.
|
|
9
|
+
*
|
|
10
|
+
* 보안 강화 (security hardening):
|
|
11
|
+
* - imageUrl: 프로토콜 allowlist (기본 https/http) — javascript:/data: 스킴 차단.
|
|
12
|
+
* `imageUrlHosts` 지정 시 호스트 allowlist 도 적용.
|
|
13
|
+
* - imageKey: 저장소 삭제 (`config.storage.deleteKeys`) 에 그대로 전달되므로
|
|
14
|
+
* 경로 순회 (`..`), 선행 슬래시, 공백/제어문자를 차단하는 형식 검증을 기본 적용.
|
|
9
15
|
*/
|
|
10
|
-
declare
|
|
16
|
+
declare const DEFAULT_IMAGE_URL_PROTOCOLS: readonly ["https:", "http:"];
|
|
17
|
+
declare const DEFAULT_IMAGE_KEY_MAX_LENGTH = 512;
|
|
18
|
+
/** 영숫자로 시작, 영숫자/`.`/`_`/`-`/`/` 만 허용. `..` 세그먼트 및 `//` 금지. */
|
|
19
|
+
declare const DEFAULT_IMAGE_KEY_PATTERN: RegExp;
|
|
20
|
+
interface CreateGallerySchemasOptions {
|
|
11
21
|
batchMax: number;
|
|
12
22
|
captionMaxLength: number;
|
|
13
|
-
|
|
23
|
+
imageUrlProtocols?: readonly string[];
|
|
24
|
+
imageUrlHosts?: readonly string[];
|
|
25
|
+
imageKeyPattern?: RegExp;
|
|
26
|
+
}
|
|
27
|
+
declare function createGallerySchemas(opts: CreateGallerySchemasOptions): {
|
|
14
28
|
CreateGallerySchema: z.ZodObject<{
|
|
15
29
|
imageUrl: z.ZodString;
|
|
16
30
|
imageKey: z.ZodOptional<z.ZodString>;
|
|
@@ -65,4 +79,4 @@ declare function createGallerySchemas(opts: {
|
|
|
65
79
|
};
|
|
66
80
|
type GallerySchemas = ReturnType<typeof createGallerySchemas>;
|
|
67
81
|
|
|
68
|
-
export { type GallerySchemas, createGallerySchemas };
|
|
82
|
+
export { type CreateGallerySchemasOptions, DEFAULT_IMAGE_KEY_MAX_LENGTH, DEFAULT_IMAGE_KEY_PATTERN, DEFAULT_IMAGE_URL_PROTOCOLS, type GallerySchemas, createGallerySchemas };
|
|
@@ -6,11 +6,25 @@ import { z } from 'zod';
|
|
|
6
6
|
* 호스트가 `config.limits.batchMax` 와 `config.limits.captionMaxLength` 를 넘겨
|
|
7
7
|
* 패키지 내부 스키마를 생성한다. categoryId 는 cuid 형식만 검증하고,
|
|
8
8
|
* 실제 카테고리 존재 여부는 service 레이어가 Prisma FK 제약으로 자연스럽게 잡는다.
|
|
9
|
+
*
|
|
10
|
+
* 보안 강화 (security hardening):
|
|
11
|
+
* - imageUrl: 프로토콜 allowlist (기본 https/http) — javascript:/data: 스킴 차단.
|
|
12
|
+
* `imageUrlHosts` 지정 시 호스트 allowlist 도 적용.
|
|
13
|
+
* - imageKey: 저장소 삭제 (`config.storage.deleteKeys`) 에 그대로 전달되므로
|
|
14
|
+
* 경로 순회 (`..`), 선행 슬래시, 공백/제어문자를 차단하는 형식 검증을 기본 적용.
|
|
9
15
|
*/
|
|
10
|
-
declare
|
|
16
|
+
declare const DEFAULT_IMAGE_URL_PROTOCOLS: readonly ["https:", "http:"];
|
|
17
|
+
declare const DEFAULT_IMAGE_KEY_MAX_LENGTH = 512;
|
|
18
|
+
/** 영숫자로 시작, 영숫자/`.`/`_`/`-`/`/` 만 허용. `..` 세그먼트 및 `//` 금지. */
|
|
19
|
+
declare const DEFAULT_IMAGE_KEY_PATTERN: RegExp;
|
|
20
|
+
interface CreateGallerySchemasOptions {
|
|
11
21
|
batchMax: number;
|
|
12
22
|
captionMaxLength: number;
|
|
13
|
-
|
|
23
|
+
imageUrlProtocols?: readonly string[];
|
|
24
|
+
imageUrlHosts?: readonly string[];
|
|
25
|
+
imageKeyPattern?: RegExp;
|
|
26
|
+
}
|
|
27
|
+
declare function createGallerySchemas(opts: CreateGallerySchemasOptions): {
|
|
14
28
|
CreateGallerySchema: z.ZodObject<{
|
|
15
29
|
imageUrl: z.ZodString;
|
|
16
30
|
imageKey: z.ZodOptional<z.ZodString>;
|
|
@@ -65,4 +79,4 @@ declare function createGallerySchemas(opts: {
|
|
|
65
79
|
};
|
|
66
80
|
type GallerySchemas = ReturnType<typeof createGallerySchemas>;
|
|
67
81
|
|
|
68
|
-
export { type GallerySchemas, createGallerySchemas };
|
|
82
|
+
export { type CreateGallerySchemasOptions, DEFAULT_IMAGE_KEY_MAX_LENGTH, DEFAULT_IMAGE_KEY_PATTERN, DEFAULT_IMAGE_URL_PROTOCOLS, type GallerySchemas, createGallerySchemas };
|
package/dist/validators/index.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
2
|
|
|
3
|
-
var _chunkILSZPLMEjs = require('../chunk-ILSZPLME.js');
|
|
4
3
|
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
|
|
6
|
+
var _chunkIYQOJJBCjs = require('../chunk-IYQOJJBC.js');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
exports.DEFAULT_IMAGE_KEY_MAX_LENGTH = _chunkIYQOJJBCjs.DEFAULT_IMAGE_KEY_MAX_LENGTH; exports.DEFAULT_IMAGE_KEY_PATTERN = _chunkIYQOJJBCjs.DEFAULT_IMAGE_KEY_PATTERN; exports.DEFAULT_IMAGE_URL_PROTOCOLS = _chunkIYQOJJBCjs.DEFAULT_IMAGE_URL_PROTOCOLS; exports.createGallerySchemas = _chunkIYQOJJBCjs.createGallerySchemas;
|
|
7
13
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-gallery/dist/validators/index.js"],"names":[],"mappings":"AAAA;AACE;AACF,uDAA6B;AAC7B;AACE;AACF
|
|
1
|
+
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-gallery/dist/validators/index.js"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACF,uDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACF,+TAAC","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-gallery/dist/validators/index.js"}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_IMAGE_KEY_MAX_LENGTH,
|
|
3
|
+
DEFAULT_IMAGE_KEY_PATTERN,
|
|
4
|
+
DEFAULT_IMAGE_URL_PROTOCOLS,
|
|
2
5
|
createGallerySchemas
|
|
3
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-IEXSUEDO.mjs";
|
|
4
7
|
export {
|
|
8
|
+
DEFAULT_IMAGE_KEY_MAX_LENGTH,
|
|
9
|
+
DEFAULT_IMAGE_KEY_PATTERN,
|
|
10
|
+
DEFAULT_IMAGE_URL_PROTOCOLS,
|
|
5
11
|
createGallerySchemas
|
|
6
12
|
};
|
|
7
13
|
//# sourceMappingURL=index.mjs.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withwiz/gallery",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Headless gallery module — Prisma 7 + Next 16, host-agnostic. Drop-in admin + public surface.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -44,7 +44,11 @@
|
|
|
44
44
|
"require": "./dist/presets/ballet.js"
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
|
-
"files": [
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"prisma",
|
|
50
|
+
"README.md"
|
|
51
|
+
],
|
|
48
52
|
"scripts": {
|
|
49
53
|
"build": "tsup",
|
|
50
54
|
"dev": "tsup --watch",
|
|
@@ -63,7 +67,9 @@
|
|
|
63
67
|
"sonner": ">=2"
|
|
64
68
|
},
|
|
65
69
|
"peerDependenciesMeta": {
|
|
66
|
-
"sonner": {
|
|
70
|
+
"sonner": {
|
|
71
|
+
"optional": true
|
|
72
|
+
}
|
|
67
73
|
},
|
|
68
74
|
"devDependencies": {
|
|
69
75
|
"@types/node": "^25.6.2",
|
package/dist/chunk-ILSZPLME.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true});// src/validators/index.ts
|
|
2
|
-
var _zod = require('zod');
|
|
3
|
-
function createGallerySchemas(opts) {
|
|
4
|
-
const CreateGallerySchema = _zod.z.object({
|
|
5
|
-
imageUrl: _zod.z.string().url(),
|
|
6
|
-
imageKey: _zod.z.string().optional(),
|
|
7
|
-
caption: _zod.z.string().max(opts.captionMaxLength).optional(),
|
|
8
|
-
categoryId: _zod.z.string().cuid(),
|
|
9
|
-
sortOrder: _zod.z.number().int().nonnegative().default(0),
|
|
10
|
-
featured: _zod.z.boolean().default(false),
|
|
11
|
-
published: _zod.z.boolean().default(false)
|
|
12
|
-
});
|
|
13
|
-
const UpdateGallerySchema = CreateGallerySchema.partial();
|
|
14
|
-
const BatchCreateGallerySchema = _zod.z.object({
|
|
15
|
-
items: _zod.z.array(CreateGallerySchema).min(1).max(opts.batchMax)
|
|
16
|
-
});
|
|
17
|
-
const BulkUpdateSchema = _zod.z.object({
|
|
18
|
-
ids: _zod.z.array(_zod.z.string().cuid()).min(1).max(opts.batchMax),
|
|
19
|
-
published: _zod.z.boolean().optional(),
|
|
20
|
-
featured: _zod.z.boolean().optional()
|
|
21
|
-
});
|
|
22
|
-
const CreateCategorySchema = _zod.z.object({
|
|
23
|
-
slug: _zod.z.string().min(1).max(64).regex(/^[A-Z][A-Z0-9_]*$/, "slug \uB294 \uB300\uBB38\uC790/\uC22B\uC790/\uC5B8\uB354\uC2A4\uCF54\uC5B4\uB9CC"),
|
|
24
|
-
labelKo: _zod.z.string().min(1).max(64),
|
|
25
|
-
labelEn: _zod.z.string().min(1).max(64).optional(),
|
|
26
|
-
sortOrder: _zod.z.number().int().nonnegative().default(0),
|
|
27
|
-
isActive: _zod.z.boolean().default(true)
|
|
28
|
-
});
|
|
29
|
-
const UpdateCategorySchema = CreateCategorySchema.partial();
|
|
30
|
-
const ReorderCategorySchema = _zod.z.object({
|
|
31
|
-
ids: _zod.z.array(_zod.z.string().cuid()).min(1)
|
|
32
|
-
});
|
|
33
|
-
return {
|
|
34
|
-
CreateGallerySchema,
|
|
35
|
-
UpdateGallerySchema,
|
|
36
|
-
BatchCreateGallerySchema,
|
|
37
|
-
BulkUpdateSchema,
|
|
38
|
-
CreateCategorySchema,
|
|
39
|
-
UpdateCategorySchema,
|
|
40
|
-
ReorderCategorySchema
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
exports.createGallerySchemas = createGallerySchemas;
|
|
47
|
-
//# sourceMappingURL=chunk-ILSZPLME.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-gallery/dist/chunk-ILSZPLME.js","../src/validators/index.ts"],"names":[],"mappings":"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","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-gallery/dist/chunk-ILSZPLME.js","sourcesContent":[null,"import { z } from \"zod\";\n\n/**\n * spec §9-1 충실 구현.\n *\n * 호스트가 `config.limits.batchMax` 와 `config.limits.captionMaxLength` 를 넘겨\n * 패키지 내부 스키마를 생성한다. categoryId 는 cuid 형식만 검증하고,\n * 실제 카테고리 존재 여부는 service 레이어가 Prisma FK 제약으로 자연스럽게 잡는다.\n */\nexport function createGallerySchemas(opts: {\n batchMax: number;\n captionMaxLength: number;\n}) {\n const CreateGallerySchema = z.object({\n imageUrl: z.string().url(),\n imageKey: z.string().optional(),\n caption: z.string().max(opts.captionMaxLength).optional(),\n categoryId: z.string().cuid(),\n sortOrder: z.number().int().nonnegative().default(0),\n featured: z.boolean().default(false),\n published: z.boolean().default(false),\n });\n\n const UpdateGallerySchema = CreateGallerySchema.partial();\n\n const BatchCreateGallerySchema = z.object({\n items: z.array(CreateGallerySchema).min(1).max(opts.batchMax),\n });\n\n const BulkUpdateSchema = z.object({\n ids: z.array(z.string().cuid()).min(1).max(opts.batchMax),\n published: z.boolean().optional(),\n featured: z.boolean().optional(),\n });\n\n const CreateCategorySchema = z.object({\n slug: z\n .string()\n .min(1)\n .max(64)\n .regex(/^[A-Z][A-Z0-9_]*$/, \"slug 는 대문자/숫자/언더스코어만\"),\n labelKo: z.string().min(1).max(64),\n labelEn: z.string().min(1).max(64).optional(),\n sortOrder: z.number().int().nonnegative().default(0),\n isActive: z.boolean().default(true),\n });\n\n const UpdateCategorySchema = CreateCategorySchema.partial();\n\n const ReorderCategorySchema = z.object({\n ids: z.array(z.string().cuid()).min(1),\n });\n\n return {\n CreateGallerySchema,\n UpdateGallerySchema,\n BatchCreateGallerySchema,\n BulkUpdateSchema,\n CreateCategorySchema,\n UpdateCategorySchema,\n ReorderCategorySchema,\n };\n}\n\nexport type GallerySchemas = ReturnType<typeof createGallerySchemas>;\n"]}
|
package/dist/chunk-YUMB3GBN.mjs
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// src/validators/index.ts
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
function createGallerySchemas(opts) {
|
|
4
|
-
const CreateGallerySchema = z.object({
|
|
5
|
-
imageUrl: z.string().url(),
|
|
6
|
-
imageKey: z.string().optional(),
|
|
7
|
-
caption: z.string().max(opts.captionMaxLength).optional(),
|
|
8
|
-
categoryId: z.string().cuid(),
|
|
9
|
-
sortOrder: z.number().int().nonnegative().default(0),
|
|
10
|
-
featured: z.boolean().default(false),
|
|
11
|
-
published: z.boolean().default(false)
|
|
12
|
-
});
|
|
13
|
-
const UpdateGallerySchema = CreateGallerySchema.partial();
|
|
14
|
-
const BatchCreateGallerySchema = z.object({
|
|
15
|
-
items: z.array(CreateGallerySchema).min(1).max(opts.batchMax)
|
|
16
|
-
});
|
|
17
|
-
const BulkUpdateSchema = z.object({
|
|
18
|
-
ids: z.array(z.string().cuid()).min(1).max(opts.batchMax),
|
|
19
|
-
published: z.boolean().optional(),
|
|
20
|
-
featured: z.boolean().optional()
|
|
21
|
-
});
|
|
22
|
-
const CreateCategorySchema = z.object({
|
|
23
|
-
slug: z.string().min(1).max(64).regex(/^[A-Z][A-Z0-9_]*$/, "slug \uB294 \uB300\uBB38\uC790/\uC22B\uC790/\uC5B8\uB354\uC2A4\uCF54\uC5B4\uB9CC"),
|
|
24
|
-
labelKo: z.string().min(1).max(64),
|
|
25
|
-
labelEn: z.string().min(1).max(64).optional(),
|
|
26
|
-
sortOrder: z.number().int().nonnegative().default(0),
|
|
27
|
-
isActive: z.boolean().default(true)
|
|
28
|
-
});
|
|
29
|
-
const UpdateCategorySchema = CreateCategorySchema.partial();
|
|
30
|
-
const ReorderCategorySchema = z.object({
|
|
31
|
-
ids: z.array(z.string().cuid()).min(1)
|
|
32
|
-
});
|
|
33
|
-
return {
|
|
34
|
-
CreateGallerySchema,
|
|
35
|
-
UpdateGallerySchema,
|
|
36
|
-
BatchCreateGallerySchema,
|
|
37
|
-
BulkUpdateSchema,
|
|
38
|
-
CreateCategorySchema,
|
|
39
|
-
UpdateCategorySchema,
|
|
40
|
-
ReorderCategorySchema
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export {
|
|
45
|
-
createGallerySchemas
|
|
46
|
-
};
|
|
47
|
-
//# sourceMappingURL=chunk-YUMB3GBN.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/validators/index.ts"],"sourcesContent":["import { z } from \"zod\";\n\n/**\n * spec §9-1 충실 구현.\n *\n * 호스트가 `config.limits.batchMax` 와 `config.limits.captionMaxLength` 를 넘겨\n * 패키지 내부 스키마를 생성한다. categoryId 는 cuid 형식만 검증하고,\n * 실제 카테고리 존재 여부는 service 레이어가 Prisma FK 제약으로 자연스럽게 잡는다.\n */\nexport function createGallerySchemas(opts: {\n batchMax: number;\n captionMaxLength: number;\n}) {\n const CreateGallerySchema = z.object({\n imageUrl: z.string().url(),\n imageKey: z.string().optional(),\n caption: z.string().max(opts.captionMaxLength).optional(),\n categoryId: z.string().cuid(),\n sortOrder: z.number().int().nonnegative().default(0),\n featured: z.boolean().default(false),\n published: z.boolean().default(false),\n });\n\n const UpdateGallerySchema = CreateGallerySchema.partial();\n\n const BatchCreateGallerySchema = z.object({\n items: z.array(CreateGallerySchema).min(1).max(opts.batchMax),\n });\n\n const BulkUpdateSchema = z.object({\n ids: z.array(z.string().cuid()).min(1).max(opts.batchMax),\n published: z.boolean().optional(),\n featured: z.boolean().optional(),\n });\n\n const CreateCategorySchema = z.object({\n slug: z\n .string()\n .min(1)\n .max(64)\n .regex(/^[A-Z][A-Z0-9_]*$/, \"slug 는 대문자/숫자/언더스코어만\"),\n labelKo: z.string().min(1).max(64),\n labelEn: z.string().min(1).max(64).optional(),\n sortOrder: z.number().int().nonnegative().default(0),\n isActive: z.boolean().default(true),\n });\n\n const UpdateCategorySchema = CreateCategorySchema.partial();\n\n const ReorderCategorySchema = z.object({\n ids: z.array(z.string().cuid()).min(1),\n });\n\n return {\n CreateGallerySchema,\n UpdateGallerySchema,\n BatchCreateGallerySchema,\n BulkUpdateSchema,\n CreateCategorySchema,\n UpdateCategorySchema,\n ReorderCategorySchema,\n };\n}\n\nexport type GallerySchemas = ReturnType<typeof createGallerySchemas>;\n"],"mappings":";AAAA,SAAS,SAAS;AASX,SAAS,qBAAqB,MAGlC;AACD,QAAM,sBAAsB,EAAE,OAAO;AAAA,IACnC,UAAU,EAAE,OAAO,EAAE,IAAI;AAAA,IACzB,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,IAC9B,SAAS,EAAE,OAAO,EAAE,IAAI,KAAK,gBAAgB,EAAE,SAAS;AAAA,IACxD,YAAY,EAAE,OAAO,EAAE,KAAK;AAAA,IAC5B,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,CAAC;AAAA,IACnD,UAAU,EAAE,QAAQ,EAAE,QAAQ,KAAK;AAAA,IACnC,WAAW,EAAE,QAAQ,EAAE,QAAQ,KAAK;AAAA,EACtC,CAAC;AAED,QAAM,sBAAsB,oBAAoB,QAAQ;AAExD,QAAM,2BAA2B,EAAE,OAAO;AAAA,IACxC,OAAO,EAAE,MAAM,mBAAmB,EAAE,IAAI,CAAC,EAAE,IAAI,KAAK,QAAQ;AAAA,EAC9D,CAAC;AAED,QAAM,mBAAmB,EAAE,OAAO;AAAA,IAChC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,EAAE,IAAI,KAAK,QAAQ;AAAA,IACxD,WAAW,EAAE,QAAQ,EAAE,SAAS;AAAA,IAChC,UAAU,EAAE,QAAQ,EAAE,SAAS;AAAA,EACjC,CAAC;AAED,QAAM,uBAAuB,EAAE,OAAO;AAAA,IACpC,MAAM,EACH,OAAO,EACP,IAAI,CAAC,EACL,IAAI,EAAE,EACN,MAAM,qBAAqB,kFAAsB;AAAA,IACpD,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE;AAAA,IACjC,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,IAC5C,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,CAAC;AAAA,IACnD,UAAU,EAAE,QAAQ,EAAE,QAAQ,IAAI;AAAA,EACpC,CAAC;AAED,QAAM,uBAAuB,qBAAqB,QAAQ;AAE1D,QAAM,wBAAwB,EAAE,OAAO;AAAA,IACrC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC;AAAA,EACvC,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|