dn-react-router-toolkit 0.8.0 → 0.8.1

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.
Files changed (47) hide show
  1. package/dist/api/index.js +13 -4
  2. package/dist/api/index.mjs +14 -5
  3. package/dist/api/item_api_handler.d.mts +4 -2
  4. package/dist/api/item_api_handler.d.ts +4 -2
  5. package/dist/api/item_api_handler.js +13 -4
  6. package/dist/api/item_api_handler.mjs +14 -5
  7. package/dist/crud/crud_loader.js +34 -5
  8. package/dist/crud/crud_loader.mjs +36 -6
  9. package/dist/crud/crud_page.js +69 -19
  10. package/dist/crud/crud_page.mjs +69 -19
  11. package/dist/crud/index.js +109 -30
  12. package/dist/crud/index.mjs +111 -31
  13. package/dist/post/index.js +6 -6
  14. package/dist/post/index.mjs +8 -7
  15. package/dist/post/post_form_page.js +6 -6
  16. package/dist/post/post_form_page.mjs +8 -7
  17. package/dist/table/index.d.mts +1 -1
  18. package/dist/table/index.d.ts +1 -1
  19. package/dist/table/index.js +94 -20
  20. package/dist/table/index.mjs +95 -20
  21. package/dist/table/load_table.d.mts +7 -1
  22. package/dist/table/load_table.d.ts +7 -1
  23. package/dist/table/load_table.js +21 -1
  24. package/dist/table/load_table.mjs +22 -1
  25. package/dist/table/loader.d.mts +3 -0
  26. package/dist/table/loader.d.ts +3 -0
  27. package/dist/table/loader.js +21 -1
  28. package/dist/table/loader.mjs +22 -1
  29. package/dist/table/page.js +69 -19
  30. package/dist/table/page.mjs +69 -19
  31. package/dist/table/repository.d.mts +6 -4
  32. package/dist/table/repository.d.ts +6 -4
  33. package/dist/table/repository.js +4 -0
  34. package/dist/table/repository.mjs +4 -0
  35. package/dist/table/table.d.mts +4 -1
  36. package/dist/table/table.d.ts +4 -1
  37. package/dist/table/table.js +55 -6
  38. package/dist/table/table.mjs +55 -6
  39. package/dist/table/table_form.d.mts +2 -2
  40. package/dist/table/table_form.d.ts +2 -2
  41. package/dist/table/table_form.js +69 -19
  42. package/dist/table/table_form.mjs +69 -19
  43. package/dist/table/use_table.d.mts +3 -3
  44. package/dist/table/use_table.d.ts +3 -3
  45. package/dist/table/use_table.js +1 -10
  46. package/dist/table/use_table.mjs +1 -10
  47. package/package.json +2 -2
package/dist/api/index.js CHANGED
@@ -379,18 +379,27 @@ function apiHandler({
379
379
  var import_http3 = require("dn-react-toolkit/http");
380
380
  function itemApiHandler({
381
381
  withAuthAction,
382
- repository
382
+ repository,
383
+ isOwnedBy,
384
+ roles
383
385
  }) {
384
386
  const loader = async ({ request }) => {
385
387
  return {};
386
388
  };
387
389
  const action = withAuthAction((auth) => async ({ params, request }) => {
388
- if (!auth || auth.role !== "admin") {
389
- return (0, import_http3.UNAUTHORIZED)();
390
+ if (roles && roles.length > 0 && (!auth || !roles.includes(auth.role))) {
391
+ throw (0, import_http3.UNAUTHORIZED)();
392
+ }
393
+ const itemId = params.itemId;
394
+ const existing = await repository.find(itemId);
395
+ if (!existing) {
396
+ throw (0, import_http3.NOT_FOUND)();
397
+ }
398
+ if (isOwnedBy && !isOwnedBy(existing, auth)) {
399
+ throw (0, import_http3.FORBIDDEN)();
390
400
  }
391
401
  switch (request.method) {
392
402
  case "DELETE": {
393
- const itemId = params.itemId;
394
403
  await repository.delete(itemId);
395
404
  return {};
396
405
  }
@@ -371,21 +371,30 @@ function apiHandler({
371
371
  }
372
372
 
373
373
  // src/api/item_api_handler.ts
374
- import { UNAUTHORIZED as UNAUTHORIZED2 } from "dn-react-toolkit/http";
374
+ import { FORBIDDEN, NOT_FOUND as NOT_FOUND2, UNAUTHORIZED as UNAUTHORIZED2 } from "dn-react-toolkit/http";
375
375
  function itemApiHandler({
376
376
  withAuthAction,
377
- repository
377
+ repository,
378
+ isOwnedBy,
379
+ roles
378
380
  }) {
379
381
  const loader = async ({ request }) => {
380
382
  return {};
381
383
  };
382
384
  const action = withAuthAction((auth) => async ({ params, request }) => {
383
- if (!auth || auth.role !== "admin") {
384
- return UNAUTHORIZED2();
385
+ if (roles && roles.length > 0 && (!auth || !roles.includes(auth.role))) {
386
+ throw UNAUTHORIZED2();
387
+ }
388
+ const itemId = params.itemId;
389
+ const existing = await repository.find(itemId);
390
+ if (!existing) {
391
+ throw NOT_FOUND2();
392
+ }
393
+ if (isOwnedBy && !isOwnedBy(existing, auth)) {
394
+ throw FORBIDDEN();
385
395
  }
386
396
  switch (request.method) {
387
397
  case "DELETE": {
388
- const itemId = params.itemId;
389
398
  await repository.delete(itemId);
390
399
  return {};
391
400
  }
@@ -2,16 +2,18 @@ import { LoaderFunctionArgs } from 'react-router';
2
2
  import { TableRepository } from '../table/repository.mjs';
3
3
  import { PgTableWithColumns } from 'drizzle-orm/pg-core';
4
4
  import { WithAuthHandler } from '../auth/with_auth.mjs';
5
+ import { AccessTokenPayload } from 'dn-react-toolkit/auth';
5
6
  import 'drizzle-orm';
6
7
  import 'drizzle-orm/node-postgres';
7
- import 'dn-react-toolkit/auth';
8
8
  import 'dn-react-toolkit/auth/server';
9
9
 
10
10
  type ItemAPIHandlerOptions<T extends PgTableWithColumns<any>, TSelect> = {
11
11
  withAuthAction: WithAuthHandler<LoaderFunctionArgs>;
12
12
  repository: TableRepository<T, TSelect>;
13
+ isOwnedBy?: (item: TSelect, auth: AccessTokenPayload | undefined) => boolean;
14
+ roles?: string[];
13
15
  };
14
- declare function itemApiHandler<T extends PgTableWithColumns<any>, TSelect>({ withAuthAction, repository, }: ItemAPIHandlerOptions<T, TSelect>): {
16
+ declare function itemApiHandler<T extends PgTableWithColumns<any>, TSelect>({ withAuthAction, repository, isOwnedBy, roles, }: ItemAPIHandlerOptions<T, TSelect>): {
15
17
  loader: ({ request }: LoaderFunctionArgs) => Promise<{}>;
16
18
  action: (arg: LoaderFunctionArgs<any>) => Promise<unknown> | unknown;
17
19
  };
@@ -2,16 +2,18 @@ import { LoaderFunctionArgs } from 'react-router';
2
2
  import { TableRepository } from '../table/repository.js';
3
3
  import { PgTableWithColumns } from 'drizzle-orm/pg-core';
4
4
  import { WithAuthHandler } from '../auth/with_auth.js';
5
+ import { AccessTokenPayload } from 'dn-react-toolkit/auth';
5
6
  import 'drizzle-orm';
6
7
  import 'drizzle-orm/node-postgres';
7
- import 'dn-react-toolkit/auth';
8
8
  import 'dn-react-toolkit/auth/server';
9
9
 
10
10
  type ItemAPIHandlerOptions<T extends PgTableWithColumns<any>, TSelect> = {
11
11
  withAuthAction: WithAuthHandler<LoaderFunctionArgs>;
12
12
  repository: TableRepository<T, TSelect>;
13
+ isOwnedBy?: (item: TSelect, auth: AccessTokenPayload | undefined) => boolean;
14
+ roles?: string[];
13
15
  };
14
- declare function itemApiHandler<T extends PgTableWithColumns<any>, TSelect>({ withAuthAction, repository, }: ItemAPIHandlerOptions<T, TSelect>): {
16
+ declare function itemApiHandler<T extends PgTableWithColumns<any>, TSelect>({ withAuthAction, repository, isOwnedBy, roles, }: ItemAPIHandlerOptions<T, TSelect>): {
15
17
  loader: ({ request }: LoaderFunctionArgs) => Promise<{}>;
16
18
  action: (arg: LoaderFunctionArgs<any>) => Promise<unknown> | unknown;
17
19
  };
@@ -26,18 +26,27 @@ module.exports = __toCommonJS(item_api_handler_exports);
26
26
  var import_http = require("dn-react-toolkit/http");
27
27
  function itemApiHandler({
28
28
  withAuthAction,
29
- repository
29
+ repository,
30
+ isOwnedBy,
31
+ roles
30
32
  }) {
31
33
  const loader = async ({ request }) => {
32
34
  return {};
33
35
  };
34
36
  const action = withAuthAction((auth) => async ({ params, request }) => {
35
- if (!auth || auth.role !== "admin") {
36
- return (0, import_http.UNAUTHORIZED)();
37
+ if (roles && roles.length > 0 && (!auth || !roles.includes(auth.role))) {
38
+ throw (0, import_http.UNAUTHORIZED)();
39
+ }
40
+ const itemId = params.itemId;
41
+ const existing = await repository.find(itemId);
42
+ if (!existing) {
43
+ throw (0, import_http.NOT_FOUND)();
44
+ }
45
+ if (isOwnedBy && !isOwnedBy(existing, auth)) {
46
+ throw (0, import_http.FORBIDDEN)();
37
47
  }
38
48
  switch (request.method) {
39
49
  case "DELETE": {
40
- const itemId = params.itemId;
41
50
  await repository.delete(itemId);
42
51
  return {};
43
52
  }
@@ -1,19 +1,28 @@
1
1
  // src/api/item_api_handler.ts
2
- import { UNAUTHORIZED } from "dn-react-toolkit/http";
2
+ import { FORBIDDEN, NOT_FOUND, UNAUTHORIZED } from "dn-react-toolkit/http";
3
3
  function itemApiHandler({
4
4
  withAuthAction,
5
- repository
5
+ repository,
6
+ isOwnedBy,
7
+ roles
6
8
  }) {
7
9
  const loader = async ({ request }) => {
8
10
  return {};
9
11
  };
10
12
  const action = withAuthAction((auth) => async ({ params, request }) => {
11
- if (!auth || auth.role !== "admin") {
12
- return UNAUTHORIZED();
13
+ if (roles && roles.length > 0 && (!auth || !roles.includes(auth.role))) {
14
+ throw UNAUTHORIZED();
15
+ }
16
+ const itemId = params.itemId;
17
+ const existing = await repository.find(itemId);
18
+ if (!existing) {
19
+ throw NOT_FOUND();
20
+ }
21
+ if (isOwnedBy && !isOwnedBy(existing, auth)) {
22
+ throw FORBIDDEN();
13
23
  }
14
24
  switch (request.method) {
15
25
  case "DELETE": {
16
- const itemId = params.itemId;
17
26
  await repository.delete(itemId);
18
27
  return {};
19
28
  }
@@ -38,11 +38,22 @@ async function loadTable({
38
38
  const offset = Number(searchParams.get("offset") ?? "0");
39
39
  const orderBy = searchParams.get("orderBy") ?? defaultOrderBy;
40
40
  const direction = searchParams.get("direction") ?? defaultDirection;
41
+ const filterWhere = Object.entries(options.filters ?? {}).map(([key, value]) => {
42
+ const param = searchParams.get(key);
43
+ if (param) {
44
+ return (0, import_drizzle_orm.eq)(
45
+ repository.schema[key],
46
+ decodeURIComponent(param)
47
+ );
48
+ }
49
+ return void 0;
50
+ }).filter(Boolean);
41
51
  const whereClauses = (0, import_drizzle_orm.and)(
42
52
  searchKey && query ? (0, import_drizzle_orm.ilike)(
43
53
  repository.schema[searchKey],
44
54
  `%${query}%`
45
55
  ) : void 0,
56
+ ...filterWhere,
46
57
  ...where ?? []
47
58
  );
48
59
  const total = await repository.countTotal({ where: whereClauses });
@@ -53,6 +64,14 @@ async function loadTable({
53
64
  offset,
54
65
  where: whereClauses
55
66
  });
67
+ const filters = Object.fromEntries(
68
+ await Promise.all(
69
+ Object.keys(options.filters ?? {}).map(async (key) => {
70
+ const values = await repository.select(key);
71
+ return [key, values.filter(Boolean)];
72
+ })
73
+ )
74
+ );
56
75
  return {
57
76
  items,
58
77
  total,
@@ -60,7 +79,8 @@ async function loadTable({
60
79
  offset,
61
80
  orderBy,
62
81
  direction,
63
- searchKey
82
+ searchKey,
83
+ filters
64
84
  };
65
85
  }
66
86
 
@@ -229,18 +249,27 @@ function apiHandler({
229
249
  var import_http2 = require("dn-react-toolkit/http");
230
250
  function itemApiHandler({
231
251
  withAuthAction,
232
- repository
252
+ repository,
253
+ isOwnedBy,
254
+ roles
233
255
  }) {
234
256
  const loader = async ({ request }) => {
235
257
  return {};
236
258
  };
237
259
  const action = withAuthAction((auth) => async ({ params, request }) => {
238
- if (!auth || auth.role !== "admin") {
239
- return (0, import_http2.UNAUTHORIZED)();
260
+ if (roles && roles.length > 0 && (!auth || !roles.includes(auth.role))) {
261
+ throw (0, import_http2.UNAUTHORIZED)();
262
+ }
263
+ const itemId = params.itemId;
264
+ const existing = await repository.find(itemId);
265
+ if (!existing) {
266
+ throw (0, import_http2.NOT_FOUND)();
267
+ }
268
+ if (isOwnedBy && !isOwnedBy(existing, auth)) {
269
+ throw (0, import_http2.FORBIDDEN)();
240
270
  }
241
271
  switch (request.method) {
242
272
  case "DELETE": {
243
- const itemId = params.itemId;
244
273
  await repository.delete(itemId);
245
274
  return {};
246
275
  }
@@ -1,6 +1,7 @@
1
1
  // src/table/load_table.tsx
2
2
  import {
3
3
  and,
4
+ eq,
4
5
  ilike
5
6
  } from "drizzle-orm";
6
7
  async function loadTable({
@@ -15,11 +16,22 @@ async function loadTable({
15
16
  const offset = Number(searchParams.get("offset") ?? "0");
16
17
  const orderBy = searchParams.get("orderBy") ?? defaultOrderBy;
17
18
  const direction = searchParams.get("direction") ?? defaultDirection;
19
+ const filterWhere = Object.entries(options.filters ?? {}).map(([key, value]) => {
20
+ const param = searchParams.get(key);
21
+ if (param) {
22
+ return eq(
23
+ repository.schema[key],
24
+ decodeURIComponent(param)
25
+ );
26
+ }
27
+ return void 0;
28
+ }).filter(Boolean);
18
29
  const whereClauses = and(
19
30
  searchKey && query ? ilike(
20
31
  repository.schema[searchKey],
21
32
  `%${query}%`
22
33
  ) : void 0,
34
+ ...filterWhere,
23
35
  ...where ?? []
24
36
  );
25
37
  const total = await repository.countTotal({ where: whereClauses });
@@ -30,6 +42,14 @@ async function loadTable({
30
42
  offset,
31
43
  where: whereClauses
32
44
  });
45
+ const filters = Object.fromEntries(
46
+ await Promise.all(
47
+ Object.keys(options.filters ?? {}).map(async (key) => {
48
+ const values = await repository.select(key);
49
+ return [key, values.filter(Boolean)];
50
+ })
51
+ )
52
+ );
33
53
  return {
34
54
  items,
35
55
  total,
@@ -37,7 +57,8 @@ async function loadTable({
37
57
  offset,
38
58
  orderBy,
39
59
  direction,
40
- searchKey
60
+ searchKey,
61
+ filters
41
62
  };
42
63
  }
43
64
 
@@ -212,21 +233,30 @@ function apiHandler({
212
233
  }
213
234
 
214
235
  // src/api/item_api_handler.ts
215
- import { UNAUTHORIZED as UNAUTHORIZED2 } from "dn-react-toolkit/http";
236
+ import { FORBIDDEN, NOT_FOUND, UNAUTHORIZED as UNAUTHORIZED2 } from "dn-react-toolkit/http";
216
237
  function itemApiHandler({
217
238
  withAuthAction,
218
- repository
239
+ repository,
240
+ isOwnedBy,
241
+ roles
219
242
  }) {
220
243
  const loader = async ({ request }) => {
221
244
  return {};
222
245
  };
223
246
  const action = withAuthAction((auth) => async ({ params, request }) => {
224
- if (!auth || auth.role !== "admin") {
225
- return UNAUTHORIZED2();
247
+ if (roles && roles.length > 0 && (!auth || !roles.includes(auth.role))) {
248
+ throw UNAUTHORIZED2();
249
+ }
250
+ const itemId = params.itemId;
251
+ const existing = await repository.find(itemId);
252
+ if (!existing) {
253
+ throw NOT_FOUND();
254
+ }
255
+ if (isOwnedBy && !isOwnedBy(existing, auth)) {
256
+ throw FORBIDDEN();
226
257
  }
227
258
  switch (request.method) {
228
259
  case "DELETE": {
229
- const itemId = params.itemId;
230
260
  await repository.delete(itemId);
231
261
  return {};
232
262
  }
@@ -195,7 +195,8 @@ function Table({
195
195
  limit,
196
196
  offset,
197
197
  orderBy,
198
- direction
198
+ direction,
199
+ filters
199
200
  }) {
200
201
  const keys = Object.entries(columns).filter((entry) => entry[1]).map(([key]) => key);
201
202
  const sortedArray = [...data];
@@ -203,7 +204,10 @@ function Table({
203
204
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
204
205
  "table",
205
206
  {
206
- className: (0, import_utils2.cn)(className, "text-[15px] border-separate border-spacing-0"),
207
+ className: (0, import_utils2.cn)(
208
+ className,
209
+ "text-[15px] border-separate border-spacing-0"
210
+ ),
207
211
  children: [
208
212
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tr", { children: keys.map((key) => {
209
213
  const value = columns[key];
@@ -220,8 +224,8 @@ function Table({
220
224
  "button",
221
225
  {
222
226
  className: (0, import_utils2.cn)(
223
- orderBy === key ? "text-neutral-900 font-medium" : "text-neutral-500",
224
- "px-4 h-14 flex items-center w-full"
227
+ orderBy === key ? "text-gray-900 font-medium" : "text-gray-500 font-medium",
228
+ "px-4 flex items-center w-full"
225
229
  ),
226
230
  onClick: () => {
227
231
  let newDirection = "asc";
@@ -242,14 +246,59 @@ function Table({
242
246
  }
243
247
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: reactNode });
244
248
  }
245
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { className: (0, import_utils2.cn)("border-y font-normal"), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Head, {}) }, key);
249
+ const filter = filters[key];
250
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
251
+ "th",
252
+ {
253
+ className: (0, import_utils2.cn)(
254
+ "py-4 border-y font-normal align-top"
255
+ ),
256
+ children: [
257
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Head, {}),
258
+ filter && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "px-3 mt-4", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
259
+ "select",
260
+ {
261
+ className: "w-full h-10 px-1.5 border rounded-full outline-none",
262
+ onChange: (e) => {
263
+ const value2 = e.target.value;
264
+ setSearchParams((prev) => {
265
+ if (value2) {
266
+ prev.set(
267
+ key,
268
+ encodeURIComponent(
269
+ value2
270
+ )
271
+ );
272
+ } else {
273
+ prev.delete(key);
274
+ }
275
+ return prev;
276
+ });
277
+ },
278
+ children: [
279
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "", children: "\uC804\uCCB4" }),
280
+ filter.map((option) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
281
+ "option",
282
+ {
283
+ value: option,
284
+ children: option
285
+ },
286
+ option
287
+ ))
288
+ ]
289
+ }
290
+ ) })
291
+ ]
292
+ },
293
+ key
294
+ );
246
295
  }) }) }),
247
296
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tbody", { children: [
248
297
  sortedArray.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tr", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
249
298
  "td",
250
299
  {
251
300
  colSpan: keys.length,
252
- className: "px-4 h-14 text-neutral-400 text-center",
301
+ className: "px-4 h-20 text-gray-400 text-center",
253
302
  children: "\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."
254
303
  }
255
304
  ) }),
@@ -288,16 +337,7 @@ function Table({
288
337
  var import_react_router3 = require("react-router");
289
338
  function useTable() {
290
339
  const { table } = (0, import_react_router3.useLoaderData)();
291
- const { items, total, limit, offset, orderBy, direction, searchKey } = table;
292
- return {
293
- items,
294
- total,
295
- limit,
296
- offset,
297
- orderBy,
298
- direction,
299
- searchKey
300
- };
340
+ return table;
301
341
  }
302
342
 
303
343
  // src/table/buttons.tsx
@@ -380,7 +420,16 @@ function TableForm({
380
420
  primaryKey = "id"
381
421
  }) {
382
422
  const { pathname } = (0, import_react_router5.useLocation)();
383
- const { items, total, limit, offset, orderBy, direction, searchKey } = useTable();
423
+ const {
424
+ items,
425
+ total,
426
+ limit,
427
+ offset,
428
+ orderBy,
429
+ direction,
430
+ searchKey,
431
+ filters
432
+ } = useTable();
384
433
  const navigate = (0, import_react_router5.useNavigate)();
385
434
  const search = (query) => {
386
435
  const searchParams2 = new URLSearchParams(window.location.search);
@@ -393,7 +442,7 @@ function TableForm({
393
442
  searchKey && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
394
443
  "form",
395
444
  {
396
- className: "h-18 px-4 flex items-center border-t",
445
+ className: "h-20 px-4 flex items-center border-t",
397
446
  onSubmit: (e) => {
398
447
  e.preventDefault();
399
448
  const formData = new FormData(e.currentTarget);
@@ -430,7 +479,8 @@ function TableForm({
430
479
  limit,
431
480
  offset,
432
481
  orderBy,
433
- direction
482
+ direction,
483
+ filters
434
484
  }
435
485
  ),
436
486
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -174,7 +174,8 @@ function Table({
174
174
  limit,
175
175
  offset,
176
176
  orderBy,
177
- direction
177
+ direction,
178
+ filters
178
179
  }) {
179
180
  const keys = Object.entries(columns).filter((entry) => entry[1]).map(([key]) => key);
180
181
  const sortedArray = [...data];
@@ -182,7 +183,10 @@ function Table({
182
183
  return /* @__PURE__ */ jsxs(
183
184
  "table",
184
185
  {
185
- className: cn(className, "text-[15px] border-separate border-spacing-0"),
186
+ className: cn(
187
+ className,
188
+ "text-[15px] border-separate border-spacing-0"
189
+ ),
186
190
  children: [
187
191
  /* @__PURE__ */ jsx2("thead", { children: /* @__PURE__ */ jsx2("tr", { children: keys.map((key) => {
188
192
  const value = columns[key];
@@ -199,8 +203,8 @@ function Table({
199
203
  "button",
200
204
  {
201
205
  className: cn(
202
- orderBy === key ? "text-neutral-900 font-medium" : "text-neutral-500",
203
- "px-4 h-14 flex items-center w-full"
206
+ orderBy === key ? "text-gray-900 font-medium" : "text-gray-500 font-medium",
207
+ "px-4 flex items-center w-full"
204
208
  ),
205
209
  onClick: () => {
206
210
  let newDirection = "asc";
@@ -221,14 +225,59 @@ function Table({
221
225
  }
222
226
  return /* @__PURE__ */ jsx2(Fragment, { children: reactNode });
223
227
  }
224
- return /* @__PURE__ */ jsx2("th", { className: cn("border-y font-normal"), children: /* @__PURE__ */ jsx2(Head, {}) }, key);
228
+ const filter = filters[key];
229
+ return /* @__PURE__ */ jsxs(
230
+ "th",
231
+ {
232
+ className: cn(
233
+ "py-4 border-y font-normal align-top"
234
+ ),
235
+ children: [
236
+ /* @__PURE__ */ jsx2(Head, {}),
237
+ filter && /* @__PURE__ */ jsx2("div", { className: "px-3 mt-4", children: /* @__PURE__ */ jsxs(
238
+ "select",
239
+ {
240
+ className: "w-full h-10 px-1.5 border rounded-full outline-none",
241
+ onChange: (e) => {
242
+ const value2 = e.target.value;
243
+ setSearchParams((prev) => {
244
+ if (value2) {
245
+ prev.set(
246
+ key,
247
+ encodeURIComponent(
248
+ value2
249
+ )
250
+ );
251
+ } else {
252
+ prev.delete(key);
253
+ }
254
+ return prev;
255
+ });
256
+ },
257
+ children: [
258
+ /* @__PURE__ */ jsx2("option", { value: "", children: "\uC804\uCCB4" }),
259
+ filter.map((option) => /* @__PURE__ */ jsx2(
260
+ "option",
261
+ {
262
+ value: option,
263
+ children: option
264
+ },
265
+ option
266
+ ))
267
+ ]
268
+ }
269
+ ) })
270
+ ]
271
+ },
272
+ key
273
+ );
225
274
  }) }) }),
226
275
  /* @__PURE__ */ jsxs("tbody", { children: [
227
276
  sortedArray.length === 0 && /* @__PURE__ */ jsx2("tr", { children: /* @__PURE__ */ jsx2(
228
277
  "td",
229
278
  {
230
279
  colSpan: keys.length,
231
- className: "px-4 h-14 text-neutral-400 text-center",
280
+ className: "px-4 h-20 text-gray-400 text-center",
232
281
  children: "\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."
233
282
  }
234
283
  ) }),
@@ -267,16 +316,7 @@ function Table({
267
316
  import { useLoaderData } from "react-router";
268
317
  function useTable() {
269
318
  const { table } = useLoaderData();
270
- const { items, total, limit, offset, orderBy, direction, searchKey } = table;
271
- return {
272
- items,
273
- total,
274
- limit,
275
- offset,
276
- orderBy,
277
- direction,
278
- searchKey
279
- };
319
+ return table;
280
320
  }
281
321
 
282
322
  // src/table/buttons.tsx
@@ -359,7 +399,16 @@ function TableForm({
359
399
  primaryKey = "id"
360
400
  }) {
361
401
  const { pathname } = useLocation2();
362
- const { items, total, limit, offset, orderBy, direction, searchKey } = useTable();
402
+ const {
403
+ items,
404
+ total,
405
+ limit,
406
+ offset,
407
+ orderBy,
408
+ direction,
409
+ searchKey,
410
+ filters
411
+ } = useTable();
363
412
  const navigate = useNavigate2();
364
413
  const search = (query) => {
365
414
  const searchParams2 = new URLSearchParams(window.location.search);
@@ -372,7 +421,7 @@ function TableForm({
372
421
  searchKey && /* @__PURE__ */ jsxs3(
373
422
  "form",
374
423
  {
375
- className: "h-18 px-4 flex items-center border-t",
424
+ className: "h-20 px-4 flex items-center border-t",
376
425
  onSubmit: (e) => {
377
426
  e.preventDefault();
378
427
  const formData = new FormData(e.currentTarget);
@@ -409,7 +458,8 @@ function TableForm({
409
458
  limit,
410
459
  offset,
411
460
  orderBy,
412
- direction
461
+ direction,
462
+ filters
413
463
  }
414
464
  ),
415
465
  /* @__PURE__ */ jsx4(