diff options
Diffstat (limited to 'lib/docu-list-rule/combo-box-settings/table')
10 files changed, 94 insertions, 1139 deletions
diff --git a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-add-dialog.tsx b/lib/docu-list-rule/combo-box-settings/table/combo-box-options-add-dialog.tsx index a5a8af2f..049e2c1a 100644 --- a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-add-dialog.tsx +++ b/lib/docu-list-rule/combo-box-settings/table/combo-box-options-add-dialog.tsx @@ -31,7 +31,7 @@ import { createComboBoxOption } from "../service" const createOptionSchema = z.object({ code: z.string().min(1, "코드는 필수입니다."), - description: z.string().default("-"), + description: z.string().default(""), remark: z.string().optional(), }) @@ -50,18 +50,24 @@ export function ComboBoxOptionsAddDialog({ codeGroupId, onSuccess }: ComboBoxOpt resolver: zodResolver(createOptionSchema), defaultValues: { code: "", - description: "-", + description: "", remark: "", }, }) + // 코드 입력 시 자동으로 description에 반영 + const handleCodeChange = (value: string) => { + form.setValue("code", value) + form.setValue("description", value) // 코드값을 description에도 자동 설정 + } + const handleSubmit = (data: CreateOptionSchema) => { startTransition(async () => { try { const result = await createComboBoxOption({ codeGroupId, code: data.code, - description: data.description || "-", + description: data.description || data.code, // description이 비어있으면 code 사용 remark: data.remark, }) @@ -109,7 +115,11 @@ export function ComboBoxOptionsAddDialog({ codeGroupId, onSuccess }: ComboBoxOpt <FormItem> <FormLabel>코드</FormLabel> <FormControl> - <Input {...field} placeholder="옵션 코드" /> + <Input + {...field} + placeholder="옵션 코드" + onChange={(e) => handleCodeChange(e.target.value)} + /> </FormControl> <FormMessage /> </FormItem> @@ -120,9 +130,9 @@ export function ComboBoxOptionsAddDialog({ codeGroupId, onSuccess }: ComboBoxOpt name="description" render={({ field }) => ( <FormItem> - <FormLabel>값</FormLabel> + <FormLabel>Description</FormLabel> <FormControl> - <Input {...field} placeholder="옵션 값" /> + <Input {...field} placeholder="옵션 설명" /> </FormControl> <FormMessage /> </FormItem> diff --git a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-detail-sheet.tsx b/lib/docu-list-rule/combo-box-settings/table/combo-box-options-detail-sheet.tsx index 0b2a76a4..b62b258e 100644 --- a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-detail-sheet.tsx +++ b/lib/docu-list-rule/combo-box-settings/table/combo-box-options-detail-sheet.tsx @@ -1,22 +1,20 @@ "use client" 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 { useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel } from "@tanstack/react-table" +import { DataTableDetail } from "@/components/data-table/data-table-detail" +import { DataTableAdvancedToolbarDetail } from "@/components/data-table/data-table-advanced-toolbar-detail" import type { DataTableAdvancedFilterField, DataTableRowAction } from "@/types/table" import { Sheet, SheetContent, } from "@/components/ui/sheet" - -import { getComboBoxOptions } from "../service" -import { getColumns } from "./combo-box-options-table-columns" -import { ComboBoxOptionsEditSheet } from "./combo-box-options-edit-sheet" -import { DeleteComboBoxOptionsDialog } from "./delete-combo-box-options-dialog" -import { ComboBoxOptionsTableToolbarActions } from "./combo-box-options-table-toolbar" -import { codeGroups } from "@/db/schema/codeGroups" - +import { getComboBoxOptions } from "@/lib/docu-list-rule/combo-box-settings/service" +import { getColumns } from "@/lib/docu-list-rule/combo-box-settings/table/combo-box-options-table-columns" +import { ComboBoxOptionsEditSheet } from "@/lib/docu-list-rule/combo-box-settings/table/combo-box-options-edit-sheet" +import { DeleteComboBoxOptionsDialog } from "@/lib/docu-list-rule/combo-box-settings/table/delete-combo-box-options-dialog" +import { ComboBoxOptionsTableToolbarActions } from "@/lib/docu-list-rule/combo-box-settings/table/combo-box-options-table-toolbar" +import { codeGroups } from "@/db/schema" type ComboBoxOption = { id: number codeGroupId: number @@ -88,10 +86,6 @@ export function ComboBoxOptionsDetailSheet({ const result = await getComboBoxOptions(codeGroup.id, { page: 1, perPage: 10, - search: table.getState().globalFilter || "", - sort: table.getState().sorting, - filters: table.getState().columnFilters, - joinOperator: "and", }) if (result.success && result.data) { const optionsWithIsActive = result.data.map(option => ({ @@ -112,27 +106,25 @@ export function ComboBoxOptionsDetailSheet({ // 고급 필터 필드 설정 const advancedFilterFields: DataTableAdvancedFilterField<any>[] = [ - { id: "code", label: "코드", type: "text" }, - { id: "description", label: "값", type: "text" }, - { id: "remark", label: "비고", type: "text" }, + { id: "code", label: "code", type: "text" }, + { id: "remark", label: "remark", type: "text" }, + { id: "updatedAt", label: "updated_at", type: "date" }, ] - const { table } = useDataTable({ + const table = useReactTable({ data: rawData.data as any, columns: columns as any, - pageCount: rawData.pageCount, - enablePinning: true, - enableAdvancedFilter: true, - manualSorting: true, - manualFiltering: true, - manualPagination: true, // 수동 페이징 활성화 + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), initialState: { - sorting: [{ id: "createdAt", desc: true }], - columnPinning: { right: ["actions"] }, + sorting: [{ id: "code", desc: false }], + pagination: { + pageSize: 10, + }, }, getRowId: (originalRow) => String((originalRow as any).id), - shallow: false, - clearOnDefault: true, }) if (!codeGroup) return null @@ -144,7 +136,7 @@ export function ComboBoxOptionsDetailSheet({ <div> <h3 className="text-lg font-medium">{codeGroup.description} 옵션 관리</h3> <p className="text-sm text-muted-foreground"> - {codeGroup.groupId}의 Combo Box 옵션들을 관리합니다. + {codeGroup.description}의 Combo Box 옵션들을 관리합니다. </p> </div> </div> @@ -155,12 +147,12 @@ export function ComboBoxOptionsDetailSheet({ onSuccess={refreshData} /> - <DataTable table={table as any}> - <DataTableAdvancedToolbar + <DataTableDetail table={table as any}> + <DataTableAdvancedToolbarDetail table={table as any} filterFields={advancedFilterFields} /> - </DataTable> + </DataTableDetail> <DeleteComboBoxOptionsDialog open={rowAction?.type === "delete"} diff --git a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-dialog.tsx b/lib/docu-list-rule/combo-box-settings/table/combo-box-options-dialog.tsx deleted file mode 100644 index 6459ae14..00000000 --- a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-dialog.tsx +++ /dev/null @@ -1,234 +0,0 @@ -"use client" - -import * as React from "react" -import { useState } from "react" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Plus, Trash2 } from "lucide-react" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table" -import { toast } from "sonner" -import { codeGroups } from "@/db/schema/codeGroups" -import { createComboBoxOption } from "../service" - -interface ComboBoxOptionsDialogProps { - open: boolean - onOpenChange: (open: boolean) => void - codeGroup: typeof codeGroups.$inferSelect | null - onSuccess: () => void -} - -interface OptionRow { - id: string - description: string - remark: string -} - -export function ComboBoxOptionsDialog({ - open, - onOpenChange, - codeGroup, - onSuccess -}: ComboBoxOptionsDialogProps) { - const [optionRows, setOptionRows] = useState<OptionRow[]>([]) - const [loading, setLoading] = useState(false) - - // 다이얼로그가 열릴 때 초기 행 생성 - React.useEffect(() => { - if (open && optionRows.length === 0) { - addRow() - } - }, [open]) - - // 새 행 추가 - const addRow = () => { - const newRow: OptionRow = { - id: `row-${Date.now()}-${Math.random()}`, - description: "", - remark: "", - } - setOptionRows(prev => [...prev, newRow]) - } - - // 행 삭제 - const removeRow = (id: string) => { - setOptionRows(prev => prev.filter(row => row.id !== id)) - } - - // 행 업데이트 - const updateRow = (id: string, field: keyof OptionRow, value: string) => { - setOptionRows(prev => - prev.map(row => - row.id === id ? { ...row, [field]: value } : row - ) - ) - } - - // 일괄 저장 - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - - if (!codeGroup) return - - // 유효한 행들만 필터링 (Description이 있는 것만) - const validRows = optionRows.filter(row => row.description.trim()) - - if (validRows.length === 0) { - toast.error("최소 하나의 Description을 입력해주세요.") - return - } - - setLoading(true) - try { - let successCount = 0 - let errorCount = 0 - - // 각 행을 순차적으로 저장 - for (const row of validRows) { - try { - const result = await createComboBoxOption({ - codeGroupId: codeGroup.id, - code: "", // 서비스에서 자동 생성 - description: row.description.trim(), - remark: row.remark.trim() || undefined, - }) - - if (result.success) { - successCount++ - } else { - errorCount++ - } - } catch (error) { - console.error("옵션 추가 실패:", error) - errorCount++ - } - } - - if (successCount > 0) { - toast.success(`${successCount}개의 옵션이 추가되었습니다.${errorCount > 0 ? ` (${errorCount}개 실패)` : ''}`) - // 폼 초기화 - setOptionRows([]) - onSuccess() - } else { - toast.error("모든 옵션 추가에 실패했습니다.") - } - } catch (error) { - console.error("옵션 추가 실패:", error) - toast.error("옵션 추가에 실패했습니다.") - } finally { - setLoading(false) - } - } - - const handleCancel = () => { - // 폼 초기화 - setOptionRows([]) - onOpenChange(false) - } - - return ( - <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="sm:max-w-[600px] max-h-[80vh] overflow-y-auto"> - <DialogHeader> - <DialogTitle>Combo Box 옵션 추가</DialogTitle> - <DialogDescription> - {codeGroup?.description}에 새로운 옵션들을 추가합니다. Code는 자동으로 생성됩니다. - </DialogDescription> - </DialogHeader> - - <div className="space-y-4"> - {/* 테이블 헤더와 추가 버튼 */} - <div className="flex items-center justify-between"> - <h4 className="text-sm font-medium">옵션 목록</h4> - <Button - type="button" - variant="outline" - size="sm" - onClick={addRow} - className="h-8" - > - <Plus className="h-4 w-4 mr-1" /> - 행 추가 - </Button> - </div> - - {/* 옵션 테이블 - 항상 표시 */} - <div className="border rounded-lg overflow-hidden"> - <Table> - <TableHeader> - <TableRow className="bg-muted/30"> - <TableHead className="w-[50%]">Description *</TableHead> - <TableHead className="w-[40%]">Remark</TableHead> - <TableHead className="w-[10%]"></TableHead> - </TableRow> - </TableHeader> - <TableBody> - {optionRows.map((row) => ( - <TableRow key={row.id} className="hover:bg-muted/30"> - <TableCell> - <Input - value={row.description} - onChange={(e) => updateRow(row.id, "description", e.target.value)} - className="border-0 focus-visible:ring-1 bg-transparent h-8" - /> - </TableCell> - <TableCell> - <Input - value={row.remark} - onChange={(e) => updateRow(row.id, "remark", e.target.value)} - className="border-0 focus-visible:ring-1 bg-transparent h-8" - /> - </TableCell> - <TableCell> - <Button - type="button" - variant="ghost" - size="sm" - onClick={() => removeRow(row.id)} - className="h-6 w-6 p-0" - > - <Trash2 className="h-3 w-3" /> - </Button> - </TableCell> - </TableRow> - ))} - </TableBody> - </Table> - </div> - </div> - - <DialogFooter> - <Button - type="button" - variant="outline" - onClick={handleCancel} - disabled={loading} - > - 취소 - </Button> - <Button - type="submit" - onClick={handleSubmit} - disabled={loading} - > - {loading ? "저장 중..." : "저장"} - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - ) -}
\ No newline at end of file diff --git a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-edit-sheet.tsx b/lib/docu-list-rule/combo-box-settings/table/combo-box-options-edit-sheet.tsx index 5732674e..4ac539d0 100644 --- a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-edit-sheet.tsx +++ b/lib/docu-list-rule/combo-box-settings/table/combo-box-options-edit-sheet.tsx @@ -5,10 +5,17 @@ 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 { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { Form, FormControl, FormField, @@ -17,19 +24,12 @@ import { FormMessage, } from "@/components/ui/form" import { Input } from "@/components/ui/input" -import { - Sheet, - SheetContent, - SheetDescription, - SheetFooter, - SheetHeader, - SheetTitle, -} from "@/components/ui/sheet" import { updateComboBoxOption } from "../service" const updateOptionSchema = z.object({ - value: z.string().min(1, "값은 필수입니다."), + code: z.string().min(1, "코드는 필수입니다."), + remark: z.string().optional(), }) type UpdateOptionSchema = z.infer<typeof updateOptionSchema> @@ -63,14 +63,16 @@ export function ComboBoxOptionsEditSheet({ const form = useForm<UpdateOptionSchema>({ resolver: zodResolver(updateOptionSchema), defaultValues: { - value: "", + code: "", + remark: "", }, }) React.useEffect(() => { if (data) { form.reset({ - value: data.description, + code: data.code, + remark: data.remark || "", }) } }, [data, form]) @@ -82,7 +84,9 @@ export function ComboBoxOptionsEditSheet({ try { const result = await updateComboBoxOption({ id: data.id, - value: formData.value, + code: formData.code, + description: data.description, // 기존 description 유지 + remark: formData.remark, }) if (result.success) { @@ -117,12 +121,25 @@ export function ComboBoxOptionsEditSheet({ <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4"> <FormField control={form.control} - name="value" + name="code" + render={({ field }) => ( + <FormItem> + <FormLabel>code</FormLabel> + <FormControl> + <Input {...field} placeholder="옵션 코드" /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="remark" render={({ field }) => ( <FormItem> - <FormLabel>값</FormLabel> + <FormLabel>remark</FormLabel> <FormControl> - <Input {...field} placeholder="옵션 값" /> + <Input {...field} placeholder="비고 (선택사항)" /> </FormControl> <FormMessage /> </FormItem> @@ -133,9 +150,6 @@ export function ComboBoxOptionsEditSheet({ 취소 </Button> <Button type="submit" disabled={isPending || !form.formState.isValid}> - {isPending && ( - <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" /> - )} 수정 </Button> </SheetFooter> diff --git a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-expandable-row.tsx b/lib/docu-list-rule/combo-box-settings/table/combo-box-options-expandable-row.tsx deleted file mode 100644 index 07b63de5..00000000 --- a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-expandable-row.tsx +++ /dev/null @@ -1,263 +0,0 @@ -"use client" - -import * as React from "react" -import { useState, useEffect } from "react" -import { MoreHorizontal, Settings } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table" -import { toast } from "sonner" -import { codeGroups } from "@/db/schema/codeGroups" -import { getComboBoxOptions, updateComboBoxOption, deleteComboBoxOption } from "../service" -import { DocumentClassOptionsSheet } from "./document-class-options-sheet" - - -interface ComboBoxOptionsExpandableRowProps { - codeGroup: typeof codeGroups.$inferSelect -} - -interface ComboBoxOption { - id: number - codeGroupId: number - code: string - description: string - remark: string | null - createdAt: Date - updatedAt: Date -} - -export function ComboBoxOptionsExpandableRow({ codeGroup }: ComboBoxOptionsExpandableRowProps) { - const [options, setOptions] = useState<ComboBoxOption[]>([]) - const [loading, setLoading] = useState(true) - const [editingOption, setEditingOption] = useState<ComboBoxOption | null>(null) - const [selectedOptionForSubOptions, setSelectedOptionForSubOptions] = useState<ComboBoxOption | null>(null) - - // 옵션 목록 로드 - const loadOptions = async () => { - try { - setLoading(true) - const result = await getComboBoxOptions(codeGroup.id) - if (result.success && result.data) { - setOptions(result.data as ComboBoxOption[]) - } else { - toast.error("옵션 목록을 불러오는데 실패했습니다.") - } - } catch (error) { - console.error("옵션 로드 실패:", error) - toast.error("옵션 목록을 불러오는데 실패했습니다.") - } finally { - setLoading(false) - } - } - - useEffect(() => { - loadOptions() - }, [codeGroup.id]) - - // 기존 옵션 수정 - const handleUpdateOption = async (option: ComboBoxOption) => { - if (!option.code.trim() || !option.description.trim()) { - toast.error("Code와 Description은 필수 입력 항목입니다.") - return - } - - try { - const result = await updateComboBoxOption({ - id: option.id, - code: option.code.trim(), - description: option.description.trim(), - remark: option.remark || undefined, - }) - - if (result.success) { - await loadOptions() // 목록 새로고침 - setEditingOption(null) // 편집 모드 종료 - toast.success("옵션이 수정되었습니다.") - } else { - toast.error("옵션 수정에 실패했습니다.") - } - } catch (error) { - console.error("옵션 수정 실패:", error) - toast.error("옵션 수정에 실패했습니다.") - } - } - - // 기존 옵션 삭제 - const handleDeleteOption = async (optionId: number) => { - if (!confirm("정말로 이 옵션을 삭제하시겠습니까?")) { - return - } - - try { - const result = await deleteComboBoxOption(optionId) - if (result.success) { - await loadOptions() // 목록 새로고침 - toast.success("옵션이 삭제되었습니다.") - } else { - toast.error("옵션 삭제에 실패했습니다.") - } - } catch (error) { - console.error("옵션 삭제 실패:", error) - toast.error("옵션 삭제에 실패했습니다.") - } - } - - // Document Class인지 확인 (Description이 "Document Class"인 경우) - const isDocumentClass = codeGroup.description === "Document Class" - - return ( - <div className="bg-muted/20 border-t"> - <div className="space-y-0 ml-[60px]"> - {/* 커스텀 테이블 */} - <div className="border overflow-hidden bg-white"> - <Table className="w-full table-fixed"> - <TableHeader> - <TableRow className="bg-muted/30"> - <TableHead className="w-[20%] font-medium text-muted-foreground">Code</TableHead> - <TableHead className="w-[30%] font-medium text-muted-foreground">Description</TableHead> - <TableHead className="w-[25%] font-medium text-muted-foreground">Remark</TableHead> - {isDocumentClass && ( - <TableHead className="w-[15%] font-medium text-muted-foreground">하위 옵션</TableHead> - )} - <TableHead className="w-[10%] font-medium text-muted-foreground">작업</TableHead> - </TableRow> - </TableHeader> - <TableBody> - {/* 기존 옵션들 */} - {options.map((option) => ( - <TableRow key={option.id} className="hover:bg-muted/30 transition-colors"> - <TableCell className="font-medium text-sm"> - {editingOption?.id === option.id ? ( - <Input - value={editingOption.code} - onChange={(e) => setEditingOption(prev => prev ? { ...prev, code: e.target.value } : null)} - placeholder="Code (*)" - className="border-0 focus-visible:ring-1 bg-transparent h-8" - /> - ) : ( - option.code - )} - </TableCell> - <TableCell className="text-sm"> - {editingOption?.id === option.id ? ( - <Input - value={editingOption.description} - onChange={(e) => setEditingOption(prev => prev ? { ...prev, description: e.target.value } : null)} - placeholder="Description (*)" - className="border-0 focus-visible:ring-1 bg-transparent h-8" - /> - ) : ( - option.description - )} - </TableCell> - <TableCell className="text-sm text-muted-foreground"> - {editingOption?.id === option.id ? ( - <Input - value={editingOption.remark || ""} - onChange={(e) => setEditingOption(prev => prev ? { ...prev, remark: e.target.value } : null)} - placeholder="Remark" - className="border-0 focus-visible:ring-1 bg-transparent h-8" - /> - ) : ( - option.remark || "-" - )} - </TableCell> - {isDocumentClass && ( - <TableCell className="text-sm"> - <Button - variant="outline" - size="sm" - onClick={() => setSelectedOptionForSubOptions(option)} - className="h-6 px-2 text-xs" - > - <Settings className="h-3 w-3 mr-1" /> - 관리 - </Button> - </TableCell> - )} - <TableCell className="text-sm"> - {editingOption?.id === option.id ? ( - <div className="flex gap-1"> - <Button - onClick={() => handleUpdateOption(editingOption)} - size="sm" - variant="outline" - className="h-6 px-2 text-xs" - > - 저장 - </Button> - <Button - onClick={() => setEditingOption(null)} - size="sm" - variant="ghost" - className="h-6 px-2 text-xs" - > - 취소 - </Button> - </div> - ) : ( - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button - variant="ghost" - className="h-6 w-6 p-0" - > - <MoreHorizontal className="h-3 w-3" /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="end"> - <DropdownMenuItem onClick={() => setEditingOption(option)}> - 수정 - </DropdownMenuItem> - <DropdownMenuSeparator /> - <DropdownMenuItem onClick={() => handleDeleteOption(option.id)}> - 삭제 - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - )} - </TableCell> - </TableRow> - ))} - - {options.length === 0 && ( - <TableRow> - <TableCell colSpan={isDocumentClass ? 5 : 4} className="text-center text-muted-foreground py-8"> - 등록된 옵션이 없습니다. - </TableCell> - </TableRow> - )} - </TableBody> - </Table> - </div> - </div> - - {/* Document Class 하위 옵션 관리 시트 */} - {selectedOptionForSubOptions && ( - <DocumentClassOptionsSheet - open={!!selectedOptionForSubOptions} - onOpenChange={(open) => !open && setSelectedOptionForSubOptions(null)} - comboBoxOption={selectedOptionForSubOptions} - onSuccess={() => { - setSelectedOptionForSubOptions(null) - // 필요시 하위 옵션 목록 새로고침 - }} - /> - )} - </div> - ) -}
\ No newline at end of file diff --git a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-table-columns.tsx b/lib/docu-list-rule/combo-box-settings/table/combo-box-options-table-columns.tsx index e5780e9e..0e46c0ed 100644 --- a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-table-columns.tsx +++ b/lib/docu-list-rule/combo-box-settings/table/combo-box-options-table-columns.tsx @@ -114,40 +114,43 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<ComboBo accessorKey: "code", enableResizing: true, header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="코드" /> + <DataTableColumnHeaderSimple column={column} title="code" /> ), meta: { - excelHeader: "코드", + excelHeader: "code", type: "text", }, cell: ({ row }) => row.getValue("code") ?? "", minSize: 80 }, { - accessorKey: "description", + accessorKey: "remark", enableResizing: true, header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="값" /> + <DataTableColumnHeaderSimple column={column} title="remark" /> ), meta: { - excelHeader: "값", + excelHeader: "remark", type: "text", }, - cell: ({ row }) => row.getValue("description") ?? "", + cell: ({ row }) => row.getValue("remark") ?? "", minSize: 80 }, { - accessorKey: "remark", + accessorKey: "updatedAt", enableResizing: true, header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="비고" /> + <DataTableColumnHeaderSimple column={column} title="updated_at" /> ), meta: { - excelHeader: "비고", - type: "text", + excelHeader: "updated_at", + type: "date", }, - cell: ({ row }) => row.getValue("remark") ?? "", - minSize: 80 + cell: ({ row }) => { + const date = row.getValue("updatedAt") as Date + return date ? new Date(date).toLocaleDateString('ko-KR') : "" + }, + minSize: 100 } ] diff --git a/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table-toolbar.tsx b/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table-toolbar.tsx deleted file mode 100644 index 77cbea01..00000000 --- a/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table-toolbar.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client" - -import * as React from "react" -import { type Table } from "@tanstack/react-table" - -import { DeleteComboBoxSettingsDialog } from "./delete-combo-box-settings-dialog" -import { codeGroups } from "@/db/schema/codeGroups" - -interface ComboBoxSettingsTableToolbarActionsProps { - table: Table<typeof codeGroups.$inferSelect> - onSuccess?: () => void -} - -export function ComboBoxSettingsTableToolbarActions({ table, onSuccess }: ComboBoxSettingsTableToolbarActionsProps) { - return ( - <div className="flex items-center gap-2"> - {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */} - {table.getFilteredSelectedRowModel().rows.length > 0 ? ( - <DeleteComboBoxSettingsDialog - codeGroups={table - .getFilteredSelectedRowModel() - .rows.map((row) => row.original)} - onSuccess={() => { - table.toggleAllRowsSelected(false) - onSuccess?.() - }} - /> - ) : null} - - - </div> - ) -}
\ No newline at end of file diff --git a/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table.tsx b/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table.tsx index f6216363..42ce1a19 100644 --- a/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table.tsx +++ b/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table.tsx @@ -1,15 +1,12 @@ "use client" import * as React from "react" -import { useRouter } from "next/navigation" 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 } from "@/types/table" -import { getComboBoxCodeGroups } from "../service" import { getColumns } from "./combo-box-settings-table-columns" import { ComboBoxOptionsDetailSheet } from "./combo-box-options-detail-sheet" -import { ComboBoxSettingsTableToolbarActions } from "./combo-box-settings-table-toolbar" import { codeGroups } from "@/db/schema/docu-list-rule" interface ComboBoxSettingsTableProps { @@ -17,21 +14,15 @@ interface ComboBoxSettingsTableProps { } export function ComboBoxSettingsTable({ promises }: ComboBoxSettingsTableProps) { - const router = useRouter() + const rawData = React.use(promises!) const [isDetailSheetOpen, setIsDetailSheetOpen] = React.useState(false) const [selectedCodeGroup, setSelectedCodeGroup] = React.useState<typeof codeGroups.$inferSelect | null>(null) - - const refreshData = React.useCallback(() => { - // 전체 페이지 새로고침 대신 router.refresh() 사용 (성능 개선) - router.refresh() - }, [router]) - // Detail 버튼 클릭 핸들러 - const handleDetail = (codeGroup: typeof codeGroups.$inferSelect) => { + const handleDetail = React.useCallback((codeGroup: typeof codeGroups.$inferSelect) => { setSelectedCodeGroup(codeGroup) setIsDetailSheetOpen(true) - } + }, []) const columns = React.useMemo(() => getColumns({ onDetail: handleDetail }), [handleDetail]) @@ -44,8 +35,6 @@ export function ComboBoxSettingsTable({ promises }: ComboBoxSettingsTableProps) id: "controlType", label: "Control Type", type: "select", options: [ { label: "Textbox", value: "textbox" }, { label: "Combobox", value: "combobox" }, - { label: "Date", value: "date" }, - { label: "Number", value: "number" }, ] }, { @@ -58,14 +47,12 @@ export function ComboBoxSettingsTable({ promises }: ComboBoxSettingsTableProps) ] const { table } = useDataTable({ - data: rawData[0].data as any, + data: rawData[0].data as typeof codeGroups.$inferSelect[], columns, pageCount: rawData[0].pageCount, enablePinning: true, enableAdvancedFilter: true, - manualSorting: false, initialState: { - sorting: [{ id: "createdAt", desc: true }], columnPinning: { right: ["actions"] }, columnFilters: [ { @@ -86,7 +73,7 @@ export function ComboBoxSettingsTable({ promises }: ComboBoxSettingsTableProps) table={table} filterFields={advancedFilterFields} > - <ComboBoxSettingsTableToolbarActions table={table} onSuccess={refreshData} /> + </DataTableAdvancedToolbar> </DataTable> diff --git a/lib/docu-list-rule/combo-box-settings/table/delete-combo-box-settings-dialog.tsx b/lib/docu-list-rule/combo-box-settings/table/delete-combo-box-settings-dialog.tsx deleted file mode 100644 index 28788bd7..00000000 --- a/lib/docu-list-rule/combo-box-settings/table/delete-combo-box-settings-dialog.tsx +++ /dev/null @@ -1,85 +0,0 @@ -"use client" - -import * as React from "react" -import { useRouter } from "next/navigation" -import { AlertTriangle } from "lucide-react" - -import { Button } from "@/components/ui/button" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { deleteCodeGroup } from "@/lib/docu-list-rule/code-groups/service" -import { codeGroups } from "@/db/schema/codeGroups" - -interface DeleteComboBoxSettingsDialogProps { - codeGroups: typeof codeGroups.$inferSelect[] - onSuccess?: () => void -} - -export function DeleteComboBoxSettingsDialog({ - codeGroups, - onSuccess, -}: DeleteComboBoxSettingsDialogProps) { - const router = useRouter() - const [isDeleting, setIsDeleting] = React.useState(false) - - const handleDelete = React.useCallback(async () => { - if (codeGroups.length === 0) return - - setIsDeleting(true) - try { - for (const codeGroup of codeGroups) { - await deleteCodeGroup(codeGroup.id) - } - - router.refresh() - onSuccess?.() - } catch (error) { - console.error("Error deleting code groups:", error) - } finally { - setIsDeleting(false) - } - }, [codeGroups, router, onSuccess]) - - if (codeGroups.length === 0) { - return null - } - - return ( - <Dialog> - <DialogContent> - <DialogHeader> - <DialogTitle className="flex items-center gap-2"> - <AlertTriangle className="h-5 w-5 text-destructive" /> - Code Group 삭제 - </DialogTitle> - <DialogDescription> - 선택한 Code Group{codeGroups.length > 1 ? "들" : ""}을 삭제하시겠습니까? - <br /> - 이 작업은 되돌릴 수 없습니다. - </DialogDescription> - </DialogHeader> - <DialogFooter> - <Button - variant="outline" - disabled={isDeleting} - > - 취소 - </Button> - <Button - variant="destructive" - onClick={handleDelete} - disabled={isDeleting} - > - {isDeleting ? "삭제 중..." : "삭제"} - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - ) -}
\ No newline at end of file diff --git a/lib/docu-list-rule/combo-box-settings/table/document-class-options-sheet.tsx b/lib/docu-list-rule/combo-box-settings/table/document-class-options-sheet.tsx deleted file mode 100644 index 8585d9a3..00000000 --- a/lib/docu-list-rule/combo-box-settings/table/document-class-options-sheet.tsx +++ /dev/null @@ -1,436 +0,0 @@ -"use client" - -import * as React from "react" -import { useState, useEffect } from "react" -import { Plus, Trash2, Save, X } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, -} from "@/components/ui/sheet" -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table" -import { toast } from "sonner" -import { - getDocumentClassOptions, - createDocumentClassOption, - updateDocumentClassOption, - deleteDocumentClassOption -} from "../service" - -interface DocumentClassOptionsSheetProps { - open: boolean - onOpenChange: (open: boolean) => void - comboBoxOption: { - id: number - code: string - description: string - } - onSuccess: () => void -} - -interface DocumentClassOption { - id: number - comboBoxSettingId: number - optionValue: string - optionCode: string | null - sortOrder: number - isActive: boolean - createdAt: Date - updatedAt: Date -} - -interface NewOptionRow { - id: string - optionValue: string - optionCode: string - sortOrder: number -} - -export function DocumentClassOptionsSheet({ - open, - onOpenChange, - comboBoxOption, - onSuccess -}: DocumentClassOptionsSheetProps) { - const [options, setOptions] = useState<DocumentClassOption[]>([]) - const [loading, setLoading] = useState(true) - const [newOptionRows, setNewOptionRows] = useState<NewOptionRow[]>([]) - const [editingOption, setEditingOption] = useState<DocumentClassOption | null>(null) - - // 하위 옵션 목록 로드 - const loadOptions = async () => { - try { - setLoading(true) - const result = await getDocumentClassOptions(comboBoxOption.id) - if (result.success && result.data) { - setOptions(result.data) - } else { - toast.error("하위 옵션 목록을 불러오는데 실패했습니다.") - } - } catch (error) { - console.error("하위 옵션 로드 실패:", error) - toast.error("하위 옵션 목록을 불러오는데 실패했습니다.") - } finally { - setLoading(false) - } - } - - useEffect(() => { - if (open) { - loadOptions() - } - }, [open, comboBoxOption.id]) - - // 새 행 추가 - const addNewRow = () => { - const newRow: NewOptionRow = { - id: `new-${Date.now()}-${Math.random()}`, - optionValue: "", - optionCode: "", - sortOrder: options.length + newOptionRows.length + 1, - } - setNewOptionRows(prev => [...prev, newRow]) - } - - // 새 행 삭제 - const removeNewRow = (id: string) => { - setNewOptionRows(prev => prev.filter(row => row.id !== id)) - } - - // 새 행 업데이트 - const updateNewRow = (id: string, field: keyof NewOptionRow, value: string | number) => { - setNewOptionRows(prev => - prev.map(row => - row.id === id ? { ...row, [field]: value } : row - ) - ) - } - - // 새 하위 옵션 저장 - const handleSaveNewOptions = async () => { - const validRows = newOptionRows.filter(row => row.optionValue.trim()) - - if (validRows.length === 0) { - toast.error("최소 하나의 옵션 값을 입력해주세요.") - return - } - - try { - let successCount = 0 - let errorCount = 0 - - for (const row of validRows) { - try { - const result = await createDocumentClassOption({ - comboBoxSettingId: comboBoxOption.id, - optionValue: row.optionValue.trim(), - optionCode: row.optionCode.trim() || undefined, - sortOrder: row.sortOrder, - }) - - if (result.success) { - successCount++ - } else { - errorCount++ - } - } catch (error) { - console.error("하위 옵션 추가 실패:", error) - errorCount++ - } - } - - if (successCount > 0) { - toast.success(`${successCount}개의 하위 옵션이 추가되었습니다.${errorCount > 0 ? ` (${errorCount}개 실패)` : ''}`) - setNewOptionRows([]) - await loadOptions() - onSuccess() - } else { - toast.error("모든 하위 옵션 추가에 실패했습니다.") - } - } catch (error) { - console.error("하위 옵션 추가 실패:", error) - toast.error("하위 옵션 추가에 실패했습니다.") - } - } - - // 기존 하위 옵션 수정 - const handleUpdateOption = async (option: DocumentClassOption) => { - if (!option.optionValue.trim()) { - toast.error("옵션 값은 필수 입력 항목입니다.") - return - } - - try { - const result = await updateDocumentClassOption({ - id: option.id, - optionValue: option.optionValue.trim(), - optionCode: option.optionCode || undefined, - sortOrder: option.sortOrder, - isActive: option.isActive, - }) - - if (result.success) { - await loadOptions() - setEditingOption(null) - toast.success("하위 옵션이 수정되었습니다.") - } else { - toast.error("하위 옵션 수정에 실패했습니다.") - } - } catch (error) { - console.error("하위 옵션 수정 실패:", error) - toast.error("하위 옵션 수정에 실패했습니다.") - } - } - - // 하위 옵션 삭제 - const handleDeleteOption = async (optionId: number) => { - if (!confirm("정말로 이 하위 옵션을 삭제하시겠습니까?")) { - return - } - - try { - const result = await deleteDocumentClassOption(optionId) - if (result.success) { - await loadOptions() - toast.success("하위 옵션이 삭제되었습니다.") - } else { - toast.error("하위 옵션 삭제에 실패했습니다.") - } - } catch (error) { - console.error("하위 옵션 삭제 실패:", error) - toast.error("하위 옵션 삭제에 실패했습니다.") - } - } - - return ( - <Sheet open={open} onOpenChange={onOpenChange}> - <SheetContent className="w-[600px] sm:max-w-[600px] overflow-y-auto"> - <SheetHeader> - <SheetTitle>하위 옵션 관리</SheetTitle> - <SheetDescription> - {comboBoxOption.description} ({comboBoxOption.code})의 하위 옵션을 관리합니다. - </SheetDescription> - </SheetHeader> - - <div className="space-y-4 mt-6"> - {/* 새 하위 옵션 추가 */} - <div className="space-y-2"> - <div className="flex items-center justify-between"> - <h4 className="text-sm font-medium">새 하위 옵션 추가</h4> - <Button - type="button" - variant="outline" - size="sm" - onClick={addNewRow} - className="h-8" - > - <Plus className="h-4 w-4 mr-1" /> - 옵션 추가 - </Button> - </div> - - {newOptionRows.length > 0 && ( - <div className="border rounded-lg overflow-hidden"> - <Table> - <TableHeader> - <TableRow className="bg-muted/30"> - <TableHead className="w-[40%]">옵션 값 *</TableHead> - <TableHead className="w-[30%]">옵션 코드</TableHead> - <TableHead className="w-[20%]">순서</TableHead> - <TableHead className="w-[10%]"></TableHead> - </TableRow> - </TableHeader> - <TableBody> - {newOptionRows.map((row) => ( - <TableRow key={row.id} className="hover:bg-muted/30"> - <TableCell> - <Input - value={row.optionValue} - onChange={(e) => updateNewRow(row.id, "optionValue", e.target.value)} - placeholder="옵션 값" - className="border-0 focus-visible:ring-1 bg-transparent h-8" - /> - </TableCell> - <TableCell> - <Input - value={row.optionCode} - onChange={(e) => updateNewRow(row.id, "optionCode", e.target.value)} - placeholder="옵션 코드 (선택)" - className="border-0 focus-visible:ring-1 bg-transparent h-8" - /> - </TableCell> - <TableCell> - <Input - type="number" - value={row.sortOrder} - onChange={(e) => updateNewRow(row.id, "sortOrder", parseInt(e.target.value) || 0)} - className="border-0 focus-visible:ring-1 bg-transparent h-8" - /> - </TableCell> - <TableCell> - <Button - onClick={() => removeNewRow(row.id)} - size="sm" - variant="ghost" - className="h-6 w-6 p-0" - > - <X className="h-3 w-3" /> - </Button> - </TableCell> - </TableRow> - ))} - </TableBody> - </Table> - <div className="p-3 border-t"> - <Button - onClick={handleSaveNewOptions} - size="sm" - className="h-8" - > - <Save className="h-4 w-4 mr-1" /> - 저장 - </Button> - </div> - </div> - )} - </div> - - {/* 기존 하위 옵션 목록 */} - <div className="space-y-2"> - <h4 className="text-sm font-medium">기존 하위 옵션</h4> - <div className="border rounded-lg overflow-hidden"> - <Table> - <TableHeader> - <TableRow className="bg-muted/30"> - <TableHead className="w-[35%]">옵션 값</TableHead> - <TableHead className="w-[25%]">옵션 코드</TableHead> - <TableHead className="w-[15%]">순서</TableHead> - <TableHead className="w-[15%]">상태</TableHead> - <TableHead className="w-[10%]">작업</TableHead> - </TableRow> - </TableHeader> - <TableBody> - {loading ? ( - <TableRow> - <TableCell colSpan={5} className="text-center py-8"> - 로딩 중... - </TableCell> - </TableRow> - ) : options.length === 0 ? ( - <TableRow> - <TableCell colSpan={5} className="text-center text-muted-foreground py-8"> - 등록된 하위 옵션이 없습니다. - </TableCell> - </TableRow> - ) : ( - options.map((option) => ( - <TableRow key={option.id} className="hover:bg-muted/30"> - <TableCell className="text-sm"> - {editingOption?.id === option.id ? ( - <Input - value={editingOption.optionValue} - onChange={(e) => setEditingOption(prev => prev ? { ...prev, optionValue: e.target.value } : null)} - placeholder="옵션 값" - className="border-0 focus-visible:ring-1 bg-transparent h-8" - /> - ) : ( - option.optionValue - )} - </TableCell> - <TableCell className="text-sm"> - {editingOption?.id === option.id ? ( - <Input - value={editingOption.optionCode || ""} - onChange={(e) => setEditingOption(prev => prev ? { ...prev, optionCode: e.target.value } : null)} - placeholder="옵션 코드" - className="border-0 focus-visible:ring-1 bg-transparent h-8" - /> - ) : ( - option.optionCode || "-" - )} - </TableCell> - <TableCell className="text-sm"> - {editingOption?.id === option.id ? ( - <Input - type="number" - value={editingOption.sortOrder} - onChange={(e) => setEditingOption(prev => prev ? { ...prev, sortOrder: parseInt(e.target.value) || 0 } : null)} - className="border-0 focus-visible:ring-1 bg-transparent h-8" - /> - ) : ( - option.sortOrder - )} - </TableCell> - <TableCell className="text-sm"> - <span className={`px-2 py-1 rounded text-xs ${ - option.isActive - ? "bg-green-100 text-green-800" - : "bg-red-100 text-red-800" - }`}> - {option.isActive ? "활성" : "비활성"} - </span> - </TableCell> - <TableCell className="text-sm"> - {editingOption?.id === option.id ? ( - <div className="flex gap-1"> - <Button - onClick={() => handleUpdateOption(editingOption)} - size="sm" - variant="outline" - className="h-6 px-2 text-xs" - > - 저장 - </Button> - <Button - onClick={() => setEditingOption(null)} - size="sm" - variant="ghost" - className="h-6 px-2 text-xs" - > - 취소 - </Button> - </div> - ) : ( - <div className="flex gap-1"> - <Button - onClick={() => setEditingOption(option)} - size="sm" - variant="outline" - className="h-6 px-2 text-xs" - > - 수정 - </Button> - <Button - onClick={() => handleDeleteOption(option.id)} - size="sm" - variant="ghost" - className="h-6 px-2 text-xs text-red-600 hover:text-red-700" - > - <Trash2 className="h-3 w-3" /> - </Button> - </div> - )} - </TableCell> - </TableRow> - )) - )} - </TableBody> - </Table> - </div> - </div> - </div> - </SheetContent> - </Sheet> - ) -}
\ No newline at end of file |
