diff options
Diffstat (limited to 'lib/docu-list-rule/document-class')
7 files changed, 141 insertions, 114 deletions
diff --git a/lib/docu-list-rule/document-class/service.ts b/lib/docu-list-rule/document-class/service.ts index 2ec31ae6..378c3215 100644 --- a/lib/docu-list-rule/document-class/service.ts +++ b/lib/docu-list-rule/document-class/service.ts @@ -18,6 +18,7 @@ export async function getDocumentClassCodeGroups(input: { classId?: string description?: string isActive?: string + projectId?: string }) { try { const { page, perPage, sort, search, filters, joinOperator } = input @@ -26,10 +27,15 @@ export async function getDocumentClassCodeGroups(input: { // 기본 조건 (plant 타입 프로젝트만) let whereConditions = sql`${documentClasses.isActive} = true AND ${projects.type} = 'plant'` + // 프로젝트 ID 필터링 + if (input.projectId) { + whereConditions = sql`${whereConditions} AND ${documentClasses.projectId} = ${parseInt(input.projectId)}` + } + // 검색 조건 if (search) { const searchTerm = `%${search}%` - whereConditions = sql`${documentClasses.isActive} = true AND ${projects.type} = 'plant' AND ( + whereConditions = sql`${whereConditions} AND ( ${documentClasses.code} ILIKE ${searchTerm} OR ${documentClasses.value} ILIKE ${searchTerm} OR ${documentClasses.description} ILIKE ${searchTerm} OR @@ -363,7 +369,7 @@ export async function getDocumentClassSubOptions(documentClassId: number, input? } // 정렬 (안전한 필드 체크 적용) - let orderBy = sql`${documentClassOptions.optionCode} ASC` + let orderBy = sql`${documentClassOptions.sdq} ASC` if (sort && sort.length > 0) { const sortField = sort[0] // 안전성 체크: 필드가 실제 테이블에 존재하는지 확인 @@ -380,6 +386,7 @@ export async function getDocumentClassSubOptions(documentClassId: number, input? documentClassId: documentClassOptions.documentClassId, description: documentClassOptions.description, optionCode: documentClassOptions.optionCode, + sdq: documentClassOptions.sdq, isActive: documentClassOptions.isActive, createdAt: documentClassOptions.createdAt, updatedAt: documentClassOptions.updatedAt, @@ -448,12 +455,21 @@ export async function createDocumentClassOptionItem(input: { } } + // 해당 Document Class의 최대 sdq 값 찾기 + const maxSdqResult = await db + .select({ maxSdq: sql<number>`COALESCE(MAX(${documentClassOptions.sdq}), 0)` }) + .from(documentClassOptions) + .where(eq(documentClassOptions.documentClassId, input.documentClassId)) + + const nextSdq = (maxSdqResult[0]?.maxSdq || 0) + 1 + const [newOption] = await db .insert(documentClassOptions) .values({ documentClassId: input.documentClassId, description: userOptionCode, // 코드값을 description에도 자동 설정 optionCode: userOptionCode, + sdq: nextSdq, isActive: true, }) .returning({ id: documentClassOptions.id }) @@ -477,11 +493,10 @@ export async function createDocumentClassOptionItem(input: { // Document Class 옵션 수정 export async function updateDocumentClassOption(input: { id: number - optionCode: string + optionCode?: string + sdq?: number }) { try { - const userOptionCode = input.optionCode.toUpperCase().trim() - // 기존 옵션 조회하여 documentClassId 가져오기 const currentOption = await db .select({ documentClassId: documentClassOptions.documentClassId }) @@ -496,32 +511,47 @@ export async function updateDocumentClassOption(input: { } } - // 같은 Document Class 내에서 코드 중복 체크 (자신 제외) - const existingOption = await db - .select({ id: documentClassOptions.id }) - .from(documentClassOptions) - .where( - and( - eq(documentClassOptions.documentClassId, currentOption[0].documentClassId), - eq(documentClassOptions.optionCode, userOptionCode) + // optionCode가 제공된 경우에만 중복 체크 및 업데이트 + if (input.optionCode) { + const userOptionCode = input.optionCode.toUpperCase().trim() + + // 같은 Document Class 내에서 코드 중복 체크 (자신 제외) + const existingOption = await db + .select({ id: documentClassOptions.id }) + .from(documentClassOptions) + .where( + and( + eq(documentClassOptions.documentClassId, currentOption[0].documentClassId), + eq(documentClassOptions.optionCode, userOptionCode) + ) ) - ) - .limit(1) + .limit(1) - if (existingOption.length > 0 && existingOption[0].id !== input.id) { - return { - success: false, - error: "이미 존재하는 코드입니다." + if (existingOption.length > 0 && existingOption[0].id !== input.id) { + return { + success: false, + error: "이미 존재하는 코드입니다." + } } } + // 업데이트할 데이터 준비 + const updateData: any = { + updatedAt: new Date(), + } + + if (input.optionCode) { + updateData.description = input.optionCode.toUpperCase().trim() + updateData.optionCode = input.optionCode.toUpperCase().trim() + } + + if (input.sdq !== undefined) { + updateData.sdq = input.sdq + } + const [updatedOption] = await db .update(documentClassOptions) - .set({ - description: userOptionCode, // 코드값을 description에도 자동 설정 - optionCode: userOptionCode, - updatedAt: new Date(), - }) + .set(updateData) .where(eq(documentClassOptions.id, input.id)) .returning({ id: documentClassOptions.id }) diff --git a/lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx b/lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx index a51b0598..e2cfc39e 100644 --- a/lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx +++ b/lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx @@ -26,19 +26,12 @@ import { FormMessage, } from "@/components/ui/form" import { Input } from "@/components/ui/input" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" + import { createDocumentClassCodeGroup } from "@/lib/docu-list-rule/document-class/service" -import { getProjectLists } from "@/lib/projects/service" +import { useParams } from "next/navigation" const createDocumentClassSchema = z.object({ - projectId: z.string().min(1, "프로젝트는 필수입니다."), value: z.string().min(1, "Value는 필수입니다."), description: z.string().optional(), }) @@ -52,56 +45,27 @@ interface DocumentClassAddDialogProps { export function DocumentClassAddDialog({ onSuccess, }: DocumentClassAddDialogProps) { + const params = useParams() + const projectId = Number(params?.projectId) const [open, setOpen] = React.useState(false) const [isPending, startTransition] = React.useTransition() - const [projects, setProjects] = React.useState<Array<{ id: number; code: string; name: string; type: string }>>([]) const form = useForm<CreateDocumentClassSchema>({ resolver: zodResolver(createDocumentClassSchema), defaultValues: { - projectId: "", value: "", description: "", }, mode: "onChange" }) - // 프로젝트 목록 로드 - React.useEffect(() => { - if (open) { - const loadProjects = async () => { - try { - const result = await getProjectLists({ - page: 1, - perPage: 1000, - search: "", - sort: [], - filters: [], - joinOperator: "and", - flags: [], - code: "", - name: "", - type: "" - }) - if (result.data) { - // plant 타입의 프로젝트만 필터링 - const plantProjects = result.data.filter(project => project.type === 'plant') - setProjects(plantProjects) - } - } catch (error) { - console.error("Failed to load projects:", error) - toast.error("프로젝트 목록을 불러오는데 실패했습니다.") - } - } - loadProjects() - } - }, [open]) + async function onSubmit(input: CreateDocumentClassSchema) { startTransition(async () => { try { const result = await createDocumentClassCodeGroup({ - projectId: parseInt(input.projectId), + projectId: projectId, value: input.value, description: input.description, }) @@ -144,30 +108,7 @@ export function DocumentClassAddDialog({ </DialogHeader> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> - <FormField - control={form.control} - name="projectId" - render={({ field }) => ( - <FormItem> - <FormLabel>프로젝트 *</FormLabel> - <Select onValueChange={field.onChange} defaultValue={field.value}> - <FormControl> - <SelectTrigger> - <SelectValue placeholder="프로젝트를 선택하세요" /> - </SelectTrigger> - </FormControl> - <SelectContent> - {projects.map((project) => ( - <SelectItem key={project.id} value={project.id.toString()}> - {project.code} - {project.name} - </SelectItem> - ))} - </SelectContent> - </Select> - <FormMessage /> - </FormItem> - )} - /> + <FormField control={form.control} diff --git a/lib/docu-list-rule/document-class/table/document-class-options-detail-sheet.tsx b/lib/docu-list-rule/document-class/table/document-class-options-detail-sheet.tsx index 50e79d89..07384dd6 100644 --- a/lib/docu-list-rule/document-class/table/document-class-options-detail-sheet.tsx +++ b/lib/docu-list-rule/document-class/table/document-class-options-detail-sheet.tsx @@ -4,17 +4,21 @@ import * as React from "react" 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 { DragDropTable } from "@/lib/docu-list-rule/number-type-configs/table/drag-drop-table" import type { DataTableAdvancedFilterField, DataTableRowAction } from "@/types/table" import { Sheet, SheetContent, } from "@/components/ui/sheet" -import { getDocumentClassSubOptions } from "@/lib/docu-list-rule/document-class/service" +import { getDocumentClassSubOptions, updateDocumentClassOption } from "@/lib/docu-list-rule/document-class/service" import { getColumns } from "@/lib/docu-list-rule/document-class/table/document-class-options-table-columns" import { DocumentClassOptionEditSheet } from "@/lib/docu-list-rule/document-class/table/document-class-option-edit-sheet" import { DeleteDocumentClassOptionDialog } from "@/lib/docu-list-rule/document-class/table/delete-document-class-option-dialog" import { DocumentClassOptionsTableToolbarActions } from "@/lib/docu-list-rule/document-class/table/document-class-options-table-toolbar" import { documentClasses, documentClassOptions } from "@/db/schema/docu-list-rule" +import { DragEndEvent } from '@dnd-kit/core' +import { arrayMove } from '@dnd-kit/sortable' +import { toast } from "sonner" type DocumentClassOption = typeof documentClassOptions.$inferSelect @@ -65,6 +69,7 @@ export function DocumentClassOptionsDetailSheet({ const result = await getDocumentClassSubOptions(documentClass.id, { page: 1, perPage: 10, + sort: [{ id: "sdq", desc: false }], }) if (result.success && result.data) { setRawData({ @@ -94,7 +99,7 @@ export function DocumentClassOptionsDetailSheet({ getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), initialState: { - sorting: [{ id: "optionCode", desc: false }], + sorting: [{ id: "sdq", desc: false }], pagination: { pageSize: 10, }, @@ -102,6 +107,58 @@ export function DocumentClassOptionsDetailSheet({ getRowId: (originalRow) => String(originalRow.id), }) + // 드래그 종료 핸들러 + const handleDragEnd = React.useCallback(async (event: DragEndEvent) => { + const { active, over } = event + console.log("Drag end event:", { active, over }) + + if (active.id !== over?.id) { + const oldIndex = rawData.data.findIndex((item) => String(item.id) === active.id) + const newIndex = rawData.data.findIndex((item) => String(item.id) === over?.id) + console.log("Indices:", { oldIndex, newIndex }) + + if (oldIndex !== -1 && newIndex !== -1) { + const reorderedData = arrayMove(rawData.data, oldIndex, newIndex) + + // 새로운 순서로 sdq 값 업데이트 + const updatedOptions = reorderedData.map((item, index) => ({ + ...item, + sdq: index + 1 + })) + + // 로컬 상태 먼저 업데이트 + setRawData(prev => ({ ...prev, data: updatedOptions })) + + // 서버에 순서 업데이트 (Combo Box Settings와 같은 방식) + try { + // 모든 항목을 임시 값으로 먼저 업데이트 + for (let i = 0; i < updatedOptions.length; i++) { + const option = updatedOptions[i] + await updateDocumentClassOption({ + id: option.id, + sdq: -(i + 1), // 임시 음수 값 + }) + } + + // 최종 순서로 업데이트 + for (const option of updatedOptions) { + await updateDocumentClassOption({ + id: option.id, + sdq: option.sdq, + }) + } + + toast.success("순서가 성공적으로 변경되었습니다.") + } catch (error) { + console.error("Error updating order:", error) + toast.error("순서 변경 중 오류가 발생했습니다.") + // 에러 시 원래 데이터로 복원 + await refreshData() + } + } + } + }, [rawData.data, refreshData]) + if (!documentClass) return null return ( @@ -122,12 +179,16 @@ export function DocumentClassOptionsDetailSheet({ onSuccess={refreshData} /> - <DataTableDetail table={table}> + <DragDropTable + table={table} + data={rawData.data} + onDragEnd={handleDragEnd} + > <DataTableAdvancedToolbarDetail table={table} filterFields={advancedFilterFields} /> - </DataTableDetail> + </DragDropTable> <DeleteDocumentClassOptionDialog open={rowAction?.type === "delete"} diff --git a/lib/docu-list-rule/document-class/table/document-class-options-table-columns.tsx b/lib/docu-list-rule/document-class/table/document-class-options-table-columns.tsx index c3bf440d..c8ee4676 100644 --- a/lib/docu-list-rule/document-class/table/document-class-options-table-columns.tsx +++ b/lib/docu-list-rule/document-class/table/document-class-options-table-columns.tsx @@ -101,6 +101,19 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof // ---------------------------------------------------------------- const dataColumns: ColumnDef<typeof documentClassOptions.$inferSelect>[] = [ { + accessorKey: "sdq", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="순서" /> + ), + meta: { + excelHeader: "순서", + type: "number", + }, + cell: ({ row }) => row.getValue("sdq") ?? "", + minSize: 50 + }, + { accessorKey: "optionCode", enableResizing: true, header: ({ column }) => ( diff --git a/lib/docu-list-rule/document-class/table/document-class-table-columns.tsx b/lib/docu-list-rule/document-class/table/document-class-table-columns.tsx index ad8494c7..8c391def 100644 --- a/lib/docu-list-rule/document-class/table/document-class-table-columns.tsx +++ b/lib/docu-list-rule/document-class/table/document-class-table-columns.tsx @@ -107,20 +107,6 @@ export function getColumns({ setRowAction, onDetail }: GetColumnsProps): ColumnD // ---------------------------------------------------------------- const dataColumns: ColumnDef<typeof documentClasses.$inferSelect>[] = [ { - accessorKey: "projectCode", - enableResizing: true, - enableColumnFilter: true, - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="프로젝트 코드" /> - ), - meta: { - excelHeader: "프로젝트 코드", - type: "text", - }, - cell: ({ row }) => row.getValue("projectCode") ?? "", - minSize: 120 - }, - { accessorKey: "code", enableResizing: true, header: ({ column }) => ( diff --git a/lib/docu-list-rule/document-class/table/document-class-table.tsx b/lib/docu-list-rule/document-class/table/document-class-table.tsx index 03855fe1..11ec3a3c 100644 --- a/lib/docu-list-rule/document-class/table/document-class-table.tsx +++ b/lib/docu-list-rule/document-class/table/document-class-table.tsx @@ -59,12 +59,7 @@ export function DocumentClassTable({ promises }: DocumentClassTableProps) { }) - // 컴포넌트 마운트 후 그룹핑 설정 - React.useEffect(() => { - if (rawData[0]?.data && table.getState().grouping.length === 0) { - table.setGrouping(["projectCode"]) - } - }, [table, rawData]) + // 정렬 시 펼쳐진 상태 유지 React.useEffect(() => { diff --git a/lib/docu-list-rule/document-class/validation.ts b/lib/docu-list-rule/document-class/validation.ts index 78f87484..b69b49ea 100644 --- a/lib/docu-list-rule/document-class/validation.ts +++ b/lib/docu-list-rule/document-class/validation.ts @@ -9,4 +9,5 @@ export const searchParamsDocumentClassCache = createSearchParamsCache({ filters: getFiltersStateParser(), search: parseAsString.withDefault(""), joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + projectId: parseAsString.withDefault(""), });
\ No newline at end of file |
