From aa86729f9a2ab95346a2851e3837de1c367aae17 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 20 Jun 2025 11:37:31 +0000 Subject: (대표님) 20250620 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/incoterms/table/delete-incoterms-dialog.tsx | 154 ++++++++++++++++++ lib/incoterms/table/incoterms-add-dialog.tsx | 12 +- lib/incoterms/table/incoterms-edit-sheet.tsx | 60 ++++--- lib/incoterms/table/incoterms-table-columns.tsx | 206 ++++++++++++++++-------- lib/incoterms/table/incoterms-table-toolbar.tsx | 41 ++++- lib/incoterms/table/incoterms-table.tsx | 102 +++++++----- 6 files changed, 444 insertions(+), 131 deletions(-) create mode 100644 lib/incoterms/table/delete-incoterms-dialog.tsx (limited to 'lib/incoterms') diff --git a/lib/incoterms/table/delete-incoterms-dialog.tsx b/lib/incoterms/table/delete-incoterms-dialog.tsx new file mode 100644 index 00000000..8b91033c --- /dev/null +++ b/lib/incoterms/table/delete-incoterms-dialog.tsx @@ -0,0 +1,154 @@ +"use client" + +import * as React from "react" +import { type Row } from "@tanstack/react-table" +import { Loader, Trash } from "lucide-react" +import { toast } from "sonner" + +import { useMediaQuery } from "@/hooks/use-media-query" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" + +import { deleteIncoterm } from "../service" +import { incoterms } from "@/db/schema/procurementRFQ" + +interface DeleteIncotermsDialogProps + extends React.ComponentPropsWithoutRef { + incoterms: Row["original"][] + showTrigger?: boolean + onSuccess?: () => void +} + +export function DeleteIncotermsDialog({ + incoterms, + showTrigger = true, + onSuccess, + ...props +}: DeleteIncotermsDialogProps) { + const [isDeletePending, startDeleteTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + + function onDelete() { + startDeleteTransition(async () => { + try { + // 각 인코텀즈를 순차적으로 삭제 + for (const incoterm of incoterms) { + const result = await deleteIncoterm(incoterm.code) + if (!result.success) { + toast.error(`인코텀즈 ${incoterm.code} 삭제 실패: ${result.error}`) + return + } + } + + props.onOpenChange?.(false) + toast.success("인코텀즈가 성공적으로 삭제되었습니다.") + onSuccess?.() + } catch (error) { + console.error("Delete error:", error) + toast.error("인코텀즈 삭제 중 오류가 발생했습니다.") + } + }) + } + + if (isDesktop) { + return ( + + {showTrigger ? ( + + + + ) : null} + + + 정말로 삭제하시겠습니까? + + 이 작업은 되돌릴 수 없습니다. 선택된{" "} + {incoterms.length} + 개의 인코텀즈를 서버에서 영구적으로 삭제합니다. + + + + + + + + + + + ) + } + + return ( + + {showTrigger ? ( + + + + ) : null} + + + 정말로 삭제하시겠습니까? + + 이 작업은 되돌릴 수 없습니다. 선택된{" "} + {incoterms.length} + 개의 인코텀즈를 서버에서 영구적으로 삭제합니다. + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/lib/incoterms/table/incoterms-add-dialog.tsx b/lib/incoterms/table/incoterms-add-dialog.tsx index ef378e1e..0f7384d6 100644 --- a/lib/incoterms/table/incoterms-add-dialog.tsx +++ b/lib/incoterms/table/incoterms-add-dialog.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; +import * as z from "zod"; import { Plus, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -70,7 +70,8 @@ export function IncotermsAddDialog({ onSuccess }: IncotermsAddDialogProps) { try { const result = await createIncoterm(data); if (result.data) { - toast.success("인코텀즈가 추가되었습니다."); + toast.success("인코텀즈가 성공적으로 추가되었습니다."); + form.reset(); setOpen(false); if (onSuccess) { onSuccess(); @@ -89,16 +90,17 @@ export function IncotermsAddDialog({ onSuccess }: IncotermsAddDialogProps) { return ( - - 인코텀즈 추가 + 새 인코텀즈 추가 새로운 인코텀즈를 추가합니다. 필수 정보를 입력해주세요. + * 표시된 항목은 필수 입력사항입니다. @@ -153,7 +155,7 @@ export function IncotermsAddDialog({ onSuccess }: IncotermsAddDialogProps) { disabled={isLoading} > {isLoading && } - {isLoading ? "생성 중..." : "인코텀즈 추가"} + {isLoading ? "생성 중..." : "추가"} diff --git a/lib/incoterms/table/incoterms-edit-sheet.tsx b/lib/incoterms/table/incoterms-edit-sheet.tsx index 9cd067c7..1ae6e902 100644 --- a/lib/incoterms/table/incoterms-edit-sheet.tsx +++ b/lib/incoterms/table/incoterms-edit-sheet.tsx @@ -5,6 +5,8 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" import { toast } from "sonner" import * as z from "zod" +import { Loader } from "lucide-react" + import { Button } from "@/components/ui/button" import { Form, @@ -16,8 +18,10 @@ import { } from "@/components/ui/form" import { Sheet, + SheetClose, SheetContent, SheetDescription, + SheetFooter, SheetHeader, SheetTitle, } from "@/components/ui/sheet" @@ -37,7 +41,7 @@ type UpdateIncotermSchema = z.infer interface IncotermsEditSheetProps { open: boolean onOpenChange: (open: boolean) => void - data: typeof incoterms.$inferSelect + data: typeof incoterms.$inferSelect | null onSuccess: () => void } @@ -47,12 +51,14 @@ export function IncotermsEditSheet({ data, onSuccess, }: IncotermsEditSheetProps) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + const form = useForm({ resolver: zodResolver(updateIncotermSchema), defaultValues: { - code: data.code, - description: data.description, - isActive: data.isActive, + code: data?.code ?? "", + description: data?.description ?? "", + isActive: data?.isActive ?? true, }, mode: "onChange" }) @@ -68,14 +74,19 @@ export function IncotermsEditSheet({ }, [data, form]) async function onSubmit(input: UpdateIncotermSchema) { - try { - await updateIncoterm(data.code, input) - toast.success("수정이 완료되었습니다.") - onSuccess() - onOpenChange(false) - } catch { - toast.error("수정 중 오류가 발생했습니다.") - } + if (!data) return + + startUpdateTransition(async () => { + try { + await updateIncoterm(data.code, input) + toast.success("인코텀즈가 성공적으로 수정되었습니다.") + onSuccess() + onOpenChange(false) + } catch (error) { + console.error("Update error:", error) + toast.error("인코텀즈 수정 중 오류가 발생했습니다.") + } + }) } return ( @@ -96,7 +107,7 @@ export function IncotermsEditSheet({ 코드 - + @@ -132,12 +143,25 @@ export function IncotermsEditSheet({ )} /> -
- + + - -
+ diff --git a/lib/incoterms/table/incoterms-table-columns.tsx b/lib/incoterms/table/incoterms-table-columns.tsx index 56a44e8b..91ce4482 100644 --- a/lib/incoterms/table/incoterms-table-columns.tsx +++ b/lib/incoterms/table/incoterms-table-columns.tsx @@ -1,76 +1,71 @@ -import type { ColumnDef, Row } from "@tanstack/react-table"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Ellipsis } from "lucide-react"; +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { Ellipsis } from "lucide-react" + +import { formatDateTime } from "@/lib/utils" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, + DropdownMenuShortcut, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { incoterms } from "@/db/schema/procurementRFQ"; -import { toast } from "sonner"; -import { deleteIncoterm } from "../service"; +} from "@/components/ui/dropdown-menu" -type Incoterm = typeof incoterms.$inferSelect; +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { incoterms } from "@/db/schema/procurementRFQ" interface GetColumnsProps { - setRowAction: (action: { type: string; row: Row }) => void; - onSuccess: () => void; + setRowAction: React.Dispatch | null>> } -const handleDelete = async (code: string, onSuccess: () => void) => { - const result = await deleteIncoterm(code); - if (result.success) { - toast.success("삭제 완료"); - onSuccess(); - } else { - toast.error(result.error || "삭제 실패"); +/** + * tanstack table 컬럼 정의 + */ +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef[] { + // ---------------------------------------------------------------- + // 1) select 컬럼 (체크박스) + // ---------------------------------------------------------------- + const selectColumn: ColumnDef = { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + maxSize: 30, + enableSorting: false, + enableHiding: false, } -}; -export function getColumns({ setRowAction, onSuccess }: GetColumnsProps): ColumnDef[] { - return [ - { - id: "code", - header: () =>
코드
, - cell: ({ row }) =>
{row.original.code}
, - enableSorting: true, - enableHiding: false, - }, - { - id: "description", - header: () =>
설명
, - cell: ({ row }) =>
{row.original.description}
, - enableSorting: true, - enableHiding: false, - }, - { - id: "isActive", - header: () =>
상태
, - cell: ({ row }) => ( - - {row.original.isActive ? "활성" : "비활성"} - - ), - enableSorting: true, - enableHiding: false, - }, - { - id: "createdAt", - header: () =>
생성일
, - cell: ({ row }) => { - const value = row.original.createdAt; - const date = value ? new Date(value) : null; - return date ? date.toLocaleDateString() : ""; - }, - enableSorting: true, - enableHiding: false, - }, - { - id: "actions", - cell: ({ row }) => ( + // ---------------------------------------------------------------- + // 2) actions 컬럼 (Dropdown 메뉴) + // ---------------------------------------------------------------- + const actionsColumn: ColumnDef = { + id: "actions", + enableHiding: false, + cell: function Cell({ row }) { + return ( ); } \ No newline at end of file diff --git a/lib/incoterms/table/incoterms-table.tsx b/lib/incoterms/table/incoterms-table.tsx index c5b5bba4..c98de810 100644 --- a/lib/incoterms/table/incoterms-table.tsx +++ b/lib/incoterms/table/incoterms-table.tsx @@ -3,13 +3,16 @@ import * as React from "react"; import { useDataTable } from "@/hooks/use-data-table"; import { DataTable } from "@/components/data-table/data-table"; import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"; +import type { + DataTableAdvancedFilterField, + DataTableRowAction, +} from "@/types/table" +import { getIncoterms } from "../service"; import { getColumns } from "./incoterms-table-columns"; -import { incoterms } from "@/db/schema/procurementRFQ"; -import { IncotermsTableToolbar } from "./incoterms-table-toolbar"; -import { toast } from "sonner"; +import { DeleteIncotermsDialog } from "./delete-incoterms-dialog"; import { IncotermsEditSheet } from "./incoterms-edit-sheet"; -import { Row } from "@tanstack/react-table"; -import { getIncoterms } from "../service"; +import { IncotermsTableToolbarActions } from "./incoterms-table-toolbar"; +import { incoterms } from "@/db/schema/procurementRFQ"; interface IncotermsTableProps { promises?: Promise<[{ data: typeof incoterms.$inferSelect[]; pageCount: number }] >; @@ -17,8 +20,7 @@ interface IncotermsTableProps { export function IncotermsTable({ promises }: IncotermsTableProps) { const [rawData, setRawData] = React.useState<{ data: typeof incoterms.$inferSelect[]; pageCount: number }>({ data: [], pageCount: 0 }); - const [isEditSheetOpen, setIsEditSheetOpen] = React.useState(false); - const [selectedRow, setSelectedRow] = React.useState(null); + const [rowAction, setRowAction] = React.useState | null>(null); React.useEffect(() => { if (promises) { @@ -44,7 +46,6 @@ export function IncotermsTable({ promises }: IncotermsTableProps) { setRawData(result); } catch (error) { console.error("Error refreshing data:", error); - toast.error("데이터를 불러오는 중 오류가 발생했습니다."); } })(); } @@ -67,50 +68,71 @@ export function IncotermsTable({ promises }: IncotermsTableProps) { setRawData(result); } catch (error) { console.error("Error refreshing data:", error); - toast.error("데이터를 불러오는 중 오류가 발생했습니다."); } }, []); - const handleRowAction = async (action: { type: string; row: Row }) => { - if (action.type === "edit") { - setSelectedRow(action.row.original); - setIsEditSheetOpen(true); - } - }; + // 컬럼 설정 - 외부 파일에서 가져옴 + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) - const columns = React.useMemo(() => getColumns({ setRowAction: handleRowAction, onSuccess: refreshData }), [refreshData]); + // 고급 필터 필드 설정 + const advancedFilterFields: DataTableAdvancedFilterField[] = [ + { id: "code", label: "코드", type: "text" }, + { + id: "isActive", label: "상태", type: "select", options: [ + { label: "활성", value: "true" }, + { label: "비활성", value: "false" }, + ] + }, + { id: "description", label: "설명", type: "text" }, + { id: "createdAt", label: "생성일", type: "date" }, + ]; const { table } = useDataTable({ - data: rawData.data, - columns, - pageCount: rawData.pageCount, - filterFields: [], - enablePinning: true, - enableAdvancedFilter: true, - initialState: { - sorting: [{ id: "createdAt", desc: true }], - columnPinning: { right: ["actions"] }, - }, - getRowId: (originalRow) => String(originalRow.code), - shallow: false, - clearOnDefault: true, - }); + data: rawData.data, + columns, + pageCount: rawData.pageCount, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.code), + shallow: false, + clearOnDefault: true, + }) return ( <> - - + + - {isEditSheetOpen && selectedRow && ( - - )} + + setRowAction(null)} + incoterms={rowAction?.row.original ? [rowAction?.row.original] : []} + showTrigger={false} + onSuccess={() => { + rowAction?.row.toggleSelected(false) + refreshData() + }} + /> + + setRowAction(null)} + data={rowAction?.row.original ?? null} + onSuccess={refreshData} + /> ); } \ No newline at end of file -- cgit v1.2.3