From e0dfb55c5457aec489fc084c4567e791b4c65eb1 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 26 Mar 2025 00:37:41 +0000 Subject: 3/25 까지의 대표님 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/items/repository.ts | 125 +++++++++++++++ lib/items/service.ts | 201 ++++++++++++++++++++++++ lib/items/table/add-items-dialog.tsx | 156 ++++++++++++++++++ lib/items/table/delete-items-dialog.tsx | 149 ++++++++++++++++++ lib/items/table/feature-flags-provider.tsx | 108 +++++++++++++ lib/items/table/feature-flags.tsx | 96 +++++++++++ lib/items/table/items-table-columns.tsx | 183 +++++++++++++++++++++ lib/items/table/items-table-toolbar-actions.tsx | 67 ++++++++ lib/items/table/items-table.tsx | 139 ++++++++++++++++ lib/items/table/update-item-sheet.tsx | 178 +++++++++++++++++++++ lib/items/validations.ts | 47 ++++++ 11 files changed, 1449 insertions(+) create mode 100644 lib/items/repository.ts create mode 100644 lib/items/service.ts create mode 100644 lib/items/table/add-items-dialog.tsx create mode 100644 lib/items/table/delete-items-dialog.tsx create mode 100644 lib/items/table/feature-flags-provider.tsx create mode 100644 lib/items/table/feature-flags.tsx create mode 100644 lib/items/table/items-table-columns.tsx create mode 100644 lib/items/table/items-table-toolbar-actions.tsx create mode 100644 lib/items/table/items-table.tsx create mode 100644 lib/items/table/update-item-sheet.tsx create mode 100644 lib/items/validations.ts (limited to 'lib/items') diff --git a/lib/items/repository.ts b/lib/items/repository.ts new file mode 100644 index 00000000..550e6b1d --- /dev/null +++ b/lib/items/repository.ts @@ -0,0 +1,125 @@ +// src/lib/items/repository.ts +import db from "@/db/db"; +import { Item, items } from "@/db/schema/items"; +import { + eq, + inArray, + not, + asc, + desc, + and, + ilike, + gte, + lte, + count, + gt, +} from "drizzle-orm"; +import { PgTransaction } from "drizzle-orm/pg-core"; +export type NewItem = typeof items.$inferInsert + +/** + * 단건/복수 조회 시 공통으로 사용 가능한 SELECT 함수 예시 + * - 트랜잭션(tx)을 받아서 사용하도록 구현 + */ +export async function selectItems( + tx: PgTransaction, + params: { + where?: any; // drizzle-orm의 조건식 (and, eq...) 등 + orderBy?: (ReturnType | ReturnType)[]; + offset?: number; + limit?: number; + } +) { + const { where, orderBy, offset = 0, limit = 10 } = params; + + return tx + .select() + .from(items) + .where(where) + .orderBy(...(orderBy ?? [])) + .offset(offset) + .limit(limit); +} +/** 총 개수 count */ +export async function countItems( + tx: PgTransaction, + where?: any +) { + const res = await tx.select({ count: count() }).from(items).where(where); + return res[0]?.count ?? 0; +} + +/** 단건 Insert 예시 */ +export async function insertItem( + tx: PgTransaction, + data: NewItem // DB와 동일한 insert 가능한 타입 +) { + // returning() 사용 시 배열로 돌아오므로 [0]만 리턴 + return tx + .insert(items) + .values(data) + .returning({ id: items.id, createdAt: items.createdAt }); +} + +/** 복수 Insert 예시 */ +export async function insertItems( + tx: PgTransaction, + data: Item[] +) { + return tx.insert(items).values(data).onConflictDoNothing(); +} + + + +/** 단건 삭제 */ +export async function deleteItemById( + tx: PgTransaction, + itemId: number +) { + return tx.delete(items).where(eq(items.id, itemId)); +} + +/** 복수 삭제 */ +export async function deleteItemsByIds( + tx: PgTransaction, + ids: number[] +) { + return tx.delete(items).where(inArray(items.id, ids)); +} + +/** 전체 삭제 */ +export async function deleteAllItems( + tx: PgTransaction, +) { + return tx.delete(items); +} + +/** 단건 업데이트 */ +export async function updateItem( + tx: PgTransaction, + itemId: number, + data: Partial +) { + return tx + .update(items) + .set(data) + .where(eq(items.id, itemId)) + .returning({ id: items.id, createdAt: items.createdAt }); +} + +/** 복수 업데이트 */ +export async function updateItems( + tx: PgTransaction, + ids: number[], + data: Partial +) { + return tx + .update(items) + .set(data) + .where(inArray(items.id, ids)) + .returning({ id: items.id, createdAt: items.createdAt }); +} + +export async function findAllItems(): Promise { + return db.select().from(items).orderBy(asc(items.itemCode)); +} diff --git a/lib/items/service.ts b/lib/items/service.ts new file mode 100644 index 00000000..ef14a5f0 --- /dev/null +++ b/lib/items/service.ts @@ -0,0 +1,201 @@ +// src/lib/items/service.ts +"use server"; // Next.js 서버 액션에서 직접 import하려면 (선택) + +import { revalidateTag, unstable_noStore } from "next/cache"; +import db from "@/db/db"; +import { customAlphabet } from "nanoid"; + +import { filterColumns } from "@/lib/filter-columns"; +import { unstable_cache } from "@/lib/unstable-cache"; +import { getErrorMessage } from "@/lib/handle-error"; + +import { asc, desc, ilike, inArray, and, gte, lte, not, or } from "drizzle-orm"; +import { CreateItemSchema, GetItemsSchema, UpdateItemSchema } from "./validations"; +import { Item, items } from "@/db/schema/items"; +import { countItems, deleteItemById, deleteItemsByIds, findAllItems, insertItem, selectItems, updateItem } from "./repository"; + + +/* ----------------------------------------------------- + 1) 조회 관련 +----------------------------------------------------- */ + +/** + * 복잡한 조건으로 Item 목록을 조회 (+ pagination) 하고, + * 총 개수에 따라 pageCount를 계산해서 리턴. + * Next.js의 unstable_cache를 사용해 일정 시간 캐시. + */ +export async function getItems(input: GetItemsSchema) { + + return unstable_cache( + async () => { + try { + const offset = (input.page - 1) * input.perPage; + + // const advancedTable = input.flags.includes("advancedTable"); + const advancedTable = true; + + // advancedTable 모드면 filterColumns()로 where 절 구성 + const advancedWhere = filterColumns({ + table: items, + filters: input.filters, + joinOperator: input.joinOperator, + }); + + + let globalWhere + if (input.search) { + const s = `%${input.search}%` + globalWhere = or(ilike(items.itemCode, s), ilike(items.itemName, s) + , ilike(items.description, s) + ) + // 필요시 여러 칼럼 OR조건 (status, priority, etc) + } + + const finalWhere = and( + // advancedWhere or your existing conditions + advancedWhere, + globalWhere // and()함수로 결합 or or() 등으로 결합 + ) + + + // 아니면 ilike, inArray, gte 등으로 where 절 구성 + const where = finalWhere + + + const orderBy = + input.sort.length > 0 + ? input.sort.map((item) => + item.desc ? desc(items[item.id]) : asc(items[item.id]) + ) + : [asc(items.createdAt)]; + + // 트랜잭션 내부에서 Repository 호출 + const { data, total } = await db.transaction(async (tx) => { + const data = await selectItems(tx, { + where, + orderBy, + offset, + limit: input.perPage, + }); + + const total = await countItems(tx, where); + return { data, total }; + }); + + const pageCount = Math.ceil(total / input.perPage); + + return { data, pageCount }; + } catch (err) { + // 에러 발생 시 디폴트 + return { data: [], pageCount: 0 }; + } + }, + [JSON.stringify(input)], // 캐싱 키 + { + revalidate: 3600, + tags: ["items"], // revalidateTag("items") 호출 시 무효화 + } + )(); +} + + +/* ----------------------------------------------------- + 2) 생성(Create) +----------------------------------------------------- */ + + +/** + * Item 생성 후, (가장 오래된 Item 1개) 삭제로 + * 전체 Item 개수를 고정 + */ +export async function createItem(input: CreateItemSchema) { + unstable_noStore(); // Next.js 서버 액션 캐싱 방지 + try { + await db.transaction(async (tx) => { + // 새 Item 생성 + const [newTask] = await insertItem(tx, { + itemCode: input.itemCode, + itemName: input.itemName, + description: input.description, + }); + return newTask; + + }); + + // 캐시 무효화 + revalidateTag("items"); + + return { data: null, error: null }; + } catch (err) { + return { data: null, error: getErrorMessage(err) }; + } +} + +/* ----------------------------------------------------- + 3) 업데이트 +----------------------------------------------------- */ + +/** 단건 업데이트 */ +export async function modifyItem(input: UpdateItemSchema & { id: number }) { + unstable_noStore(); + try { + const data = await db.transaction(async (tx) => { + const [res] = await updateItem(tx, input.id, { + itemCode: input.itemCode, + itemName: input.itemName, + description: input.description, + }); + return res; + }); + + revalidateTag("items"); + return { data: null, error: null }; + } catch (err) { + return { data: null, error: getErrorMessage(err) }; + } +} + + + +/** 단건 삭제 */ +export async function removeItem(input: { id: number }) { + unstable_noStore(); + try { + await db.transaction(async (tx) => { + // 삭제 + await deleteItemById(tx, input.id); + // 바로 새 Item 생성 + }); + + revalidateTag("items"); + + return { data: null, error: null }; + } catch (err) { + return { data: null, error: getErrorMessage(err) }; + } +} + +/** 복수 삭제 */ +export async function removeItems(input: { ids: number[] }) { + unstable_noStore(); + try { + await db.transaction(async (tx) => { + // 삭제 + await deleteItemsByIds(tx, input.ids); + }); + + revalidateTag("items"); + + return { data: null, error: null }; + } catch (err) { + return { data: null, error: getErrorMessage(err) }; + } +} + +export async function getAllItems(): Promise { + try { + return await findAllItems(); + } catch (err) { + throw new Error("Failed to get roles"); + } +} diff --git a/lib/items/table/add-items-dialog.tsx b/lib/items/table/add-items-dialog.tsx new file mode 100644 index 00000000..2224444c --- /dev/null +++ b/lib/items/table/add-items-dialog.tsx @@ -0,0 +1,156 @@ +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" + +import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +// react-hook-form + shadcn/ui Form +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +// shadcn/ui Select +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { createItemSchema, CreateItemSchema } from "../validations" +import { createItem } from "../service" +import { Textarea } from "@/components/ui/textarea" + + + +export function AddItemDialog() { + const [open, setOpen] = React.useState(false) + + // react-hook-form 세팅 + const form = useForm({ + resolver: zodResolver(createItemSchema), + defaultValues: { + itemCode: "", + itemName: "", + description: "", + }, + }) + + async function onSubmit(data: CreateItemSchema) { + const result = await createItem(data) + if (result.error) { + alert(`에러: ${result.error}`) + return + } + // 성공 시 모달 닫고 폼 리셋 + form.reset() + setOpen(false) + } + + function handleDialogOpenChange(nextOpen: boolean) { + if (!nextOpen) { + form.reset() + } + setOpen(nextOpen) + } + + return ( + + {/* 모달을 열기 위한 버튼 */} + + + + + + + Create New Item + + 새 Item 정보를 입력하고 Create 버튼을 누르세요. + + + + {/* shadcn/ui Form을 이용해 react-hook-form과 연결 */} +
+ +
+ + ( + + Item Code + + + + + + )} + /> + ( + + Item Name + + + + + + )} + /> + + ( + + Description + +