diff options
Diffstat (limited to 'lib')
28 files changed, 418 insertions, 469 deletions
diff --git a/lib/docu-list-rule/code-groups/service.ts b/lib/docu-list-rule/code-groups/service.ts index c854f6c9..d9b3b859 100644 --- a/lib/docu-list-rule/code-groups/service.ts +++ b/lib/docu-list-rule/code-groups/service.ts @@ -15,6 +15,7 @@ export async function getCodeGroups(input: { filters: Array<{ id: string; value: string | string[] }> joinOperator: "and" | "or" sort: Array<{ id: string; desc: boolean }> + projectId?: string }) { unstable_noStore() @@ -25,10 +26,15 @@ export async function getCodeGroups(input: { // 검색 조건 (plant 타입 프로젝트만) let whereConditions = sql`${projects.type} = 'plant'` + // 프로젝트 ID 필터링 + if (input.projectId) { + whereConditions = sql`${whereConditions} AND ${codeGroups.projectId} = ${parseInt(input.projectId)}` + } + // 검색어 필터링 if (search) { const searchTerm = `%${search}%` - whereConditions = sql`${projects.type} = 'plant' AND ( + whereConditions = sql`${whereConditions} AND ( ${codeGroups.groupId} ILIKE ${searchTerm} OR ${codeGroups.description} ILIKE ${searchTerm} OR ${codeGroups.codeFormat} ILIKE ${searchTerm} OR diff --git a/lib/docu-list-rule/code-groups/table/code-groups-add-dialog.tsx b/lib/docu-list-rule/code-groups/table/code-groups-add-dialog.tsx index 33dfdd03..f5354161 100644 --- a/lib/docu-list-rule/code-groups/table/code-groups-add-dialog.tsx +++ b/lib/docu-list-rule/code-groups/table/code-groups-add-dialog.tsx @@ -34,11 +34,10 @@ import { SelectValue, } from "@/components/ui/select" import { createCodeGroup } from "@/lib/docu-list-rule/code-groups/service" -import { getProjectLists } from "@/lib/projects/service" +import { useParams } from "next/navigation" import { z } from "zod" const createCodeGroupSchema = z.object({ - projectId: z.string().min(1, "프로젝트는 필수입니다."), description: z.string().min(1, "Description은 필수입니다."), codeFormat: z.string().optional().refine((val) => { if (!val) return true; // 빈 값은 허용 @@ -54,51 +53,21 @@ interface CodeGroupsAddDialogProps { } export function CodeGroupsAddDialog({ onSuccess }: CodeGroupsAddDialogProps) { + const params = useParams() + const projectId = Number(params?.projectId) const [open, setOpen] = React.useState(false) const [isLoading, setIsLoading] = React.useState(false) - const [projects, setProjects] = React.useState<Array<{ id: number; code: string; name: string; type: string }>>([]) const form = useForm<CreateCodeGroupFormValues>({ resolver: zodResolver(createCodeGroupSchema), defaultValues: { - projectId: "", description: "", codeFormat: "", controlType: "", }, }) - // 프로젝트 목록 로드 - 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]) + // Code Format을 기반으로 정규식 자동 생성 함수 const generateExpression = (codeFormat: string): string => { @@ -157,7 +126,7 @@ export function CodeGroupsAddDialog({ onSuccess }: CodeGroupsAddDialogProps) { const expressions = generateExpression(data.codeFormat || "") const result = await createCodeGroup({ - projectId: parseInt(data.projectId), + projectId: projectId, description: data.description, codeFormat: data.codeFormat, expressions: expressions, @@ -201,31 +170,6 @@ export function CodeGroupsAddDialog({ onSuccess }: CodeGroupsAddDialogProps) { <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} name="description" render={({ field }) => ( <FormItem> diff --git a/lib/docu-list-rule/code-groups/table/code-groups-table-columns.tsx b/lib/docu-list-rule/code-groups/table/code-groups-table-columns.tsx index 01047c50..c15dd676 100644 --- a/lib/docu-list-rule/code-groups/table/code-groups-table-columns.tsx +++ b/lib/docu-list-rule/code-groups/table/code-groups-table-columns.tsx @@ -103,19 +103,6 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof // ---------------------------------------------------------------- const dataColumns: ColumnDef<typeof codeGroups.$inferSelect>[] = [ { - accessorKey: "projectCode", - enableResizing: true, - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="프로젝트 코드" /> - ), - meta: { - excelHeader: "프로젝트 코드", - type: "text", - }, - cell: ({ row }) => row.getValue("projectCode") ?? "", - minSize: 120 - }, - { accessorKey: "groupId", enableResizing: true, header: ({ column }) => ( diff --git a/lib/docu-list-rule/code-groups/table/code-groups-table.tsx b/lib/docu-list-rule/code-groups/table/code-groups-table.tsx index 0029ed91..fdddb2d6 100644 --- a/lib/docu-list-rule/code-groups/table/code-groups-table.tsx +++ b/lib/docu-list-rule/code-groups/table/code-groups-table.tsx @@ -75,12 +75,7 @@ export function CodeGroupsTable({ promises }: CodeGroupsTableProps) { clearOnDefault: false, }) - // 컴포넌트 마운트 후 그룹핑 설정 - React.useEffect(() => { - if (data && table.getState().grouping.length === 0) { - table.setGrouping(["projectCode"]) - } - }, [table, data]) + // 정렬 시 펼쳐진 상태 유지 React.useEffect(() => { diff --git a/lib/docu-list-rule/code-groups/validation.ts b/lib/docu-list-rule/code-groups/validation.ts index 9745841c..46ba02e8 100644 --- a/lib/docu-list-rule/code-groups/validation.ts +++ b/lib/docu-list-rule/code-groups/validation.ts @@ -24,6 +24,7 @@ export const searchParamsCodeGroupsCache = createSearchParamsCache({ description: parseAsString.withDefault(""), controlType: parseAsString.withDefault(""), isActive: parseAsString.withDefault(""), + projectId: parseAsString.withDefault(""), // advanced filter filters: getFiltersStateParser().withDefault([]), diff --git a/lib/docu-list-rule/combo-box-settings/service.ts b/lib/docu-list-rule/combo-box-settings/service.ts index c733f978..d2f7d0f7 100644 --- a/lib/docu-list-rule/combo-box-settings/service.ts +++ b/lib/docu-list-rule/combo-box-settings/service.ts @@ -19,6 +19,7 @@ export async function getComboBoxCodeGroups(input: { groupId?: string description?: string isActive?: string + projectId?: string }) { unstable_noStore() @@ -29,10 +30,15 @@ export async function getComboBoxCodeGroups(input: { // Control Type이 combobox이고 plant 타입 프로젝트인 조건 let whereConditions = sql`${codeGroups.controlType} = 'combobox' AND ${projects.type} = 'plant'` + // 프로젝트 ID 필터링 + if (input.projectId) { + whereConditions = sql`${whereConditions} AND ${codeGroups.projectId} = ${parseInt(input.projectId)}` + } + // 검색 조건 if (search) { const searchTerm = `%${search}%` - whereConditions = sql`${codeGroups.controlType} = 'combobox' AND ${projects.type} = 'plant' AND ( + whereConditions = sql`${whereConditions} AND ( ${codeGroups.groupId} ILIKE ${searchTerm} OR ${codeGroups.description} ILIKE ${searchTerm} OR ${codeGroups.codeFormat} ILIKE ${searchTerm} OR @@ -168,7 +174,7 @@ export async function getComboBoxOptions(codeGroupId: number, input?: { } // 정렬 (안전한 필드 체크 적용) - let orderBy = sql`${comboBoxSettings.code} ASC` + let orderBy = sql`${comboBoxSettings.sdq} ASC` if (sort && sort.length > 0) { const sortField = sort[0] // 안전성 체크: 필드가 실제 테이블에 존재하는지 확인 @@ -193,6 +199,7 @@ export async function getComboBoxOptions(codeGroupId: number, input?: { code: comboBoxSettings.code, description: comboBoxSettings.description, remark: comboBoxSettings.remark, + sdq: comboBoxSettings.sdq, createdAt: comboBoxSettings.createdAt, updatedAt: comboBoxSettings.updatedAt, projectCode: projects.code, @@ -274,6 +281,14 @@ export async function createComboBoxOption(input: { } } + // 다음 순서 번호 계산 + const maxSdqResult = await db + .select({ maxSdq: sql<number>`COALESCE(MAX(sdq), 0)` }) + .from(comboBoxSettings) + .where(eq(comboBoxSettings.codeGroupId, input.codeGroupId)) + + const nextSdq = (maxSdqResult[0]?.maxSdq || 0) + 1 + const [newOption] = await db .insert(comboBoxSettings) .values({ @@ -281,6 +296,7 @@ export async function createComboBoxOption(input: { code: input.code, description: input.description || "-", remark: input.remark, + sdq: nextSdq, }) .returning({ id: comboBoxSettings.id }) @@ -308,6 +324,7 @@ export async function updateComboBoxOption(input: { code: string description: string remark?: string + sdq?: number }) { try { // 현재 수정 중인 항목의 codeGroupId 가져오기 @@ -346,6 +363,7 @@ export async function updateComboBoxOption(input: { code: input.code, description: input.description, remark: input.remark, + ...(input.sdq !== undefined && { sdq: input.sdq }), updatedAt: new Date(), }) .where(eq(comboBoxSettings.id, input.id)) @@ -421,4 +439,99 @@ export async function clearComboBoxOptions(codeGroupId: number) { } } -
\ No newline at end of file +// Combo Box 옵션 순서 업데이트 (드래그 앤 드롭) +export async function updateComboBoxOptionOrder(codeGroupId: number, reorderedOptions: { id: number; sdq: number }[]) { + try { + console.log("Updating combo box option order:", { codeGroupId, reorderedOptions }) + + // 유니크 제약조건 때문에 임시로 큰 값으로 업데이트 후 실제 값으로 업데이트 + await db.transaction(async (tx) => { + // 1단계: 모든 옵션을 임시 큰 값으로 업데이트 + for (const option of reorderedOptions) { + console.log("Step 1 - Setting temporary value for option:", option.id) + await tx + .update(comboBoxSettings) + .set({ + sdq: option.sdq + 1000, // 임시로 큰 값 설정 + updatedAt: new Date(), + }) + .where(eq(comboBoxSettings.id, option.id)) + } + + // 2단계: 실제 값으로 업데이트 + for (const option of reorderedOptions) { + console.log("Step 2 - Setting final value for option:", option.id, "sdq:", option.sdq) + const result = await tx + .update(comboBoxSettings) + .set({ + sdq: option.sdq, + updatedAt: new Date(), + }) + .where(eq(comboBoxSettings.id, option.id)) + .returning({ id: comboBoxSettings.id, sdq: comboBoxSettings.sdq }) + + console.log("Update result:", result) + } + }) + + revalidatePath("/evcp/docu-list-rule/combo-box-settings") + + return { + success: true, + message: "Combo Box options reordered successfully" + } + } catch (error) { + console.error("Error updating combo box option order:", error) + return { + success: false, + error: "Failed to update combo box option order" + } + } +} + +// 기존 데이터에 기본 순서값 설정 (마이그레이션 후 한 번만 실행) +export async function initializeComboBoxOptionOrder() { + try { + // sdq가 null인 모든 옵션들을 찾아서 순서대로 업데이트 + const codeGroupsWithOptions = await db + .select({ + codeGroupId: comboBoxSettings.codeGroupId, + id: comboBoxSettings.id, + }) + .from(comboBoxSettings) + .orderBy(comboBoxSettings.codeGroupId, comboBoxSettings.createdAt) + + // codeGroupId별로 그룹화하여 순서 설정 + const groupedOptions = codeGroupsWithOptions.reduce((acc, option) => { + if (!acc[option.codeGroupId]) { + acc[option.codeGroupId] = [] + } + acc[option.codeGroupId].push(option.id) + return acc + }, {} as Record<number, number[]>) + + // 각 그룹별로 순서 업데이트 + for (const [codeGroupId, optionIds] of Object.entries(groupedOptions)) { + for (let i = 0; i < optionIds.length; i++) { + await db + .update(comboBoxSettings) + .set({ + sdq: i + 1, + updatedAt: new Date(), + }) + .where(eq(comboBoxSettings.id, optionIds[i])) + } + } + + return { + success: true, + message: "Combo Box option order initialized successfully" + } + } catch (error) { + console.error("Error initializing combo box option order:", error) + return { + success: false, + error: "Failed to initialize combo box option order" + } + } +}
\ No newline at end of file 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 286acfbf..7e81fdab 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 @@ -4,12 +4,16 @@ 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 { DragEndEvent } from '@dnd-kit/core' +import { arrayMove } from '@dnd-kit/sortable' +import { toast } from "sonner" import { Sheet, SheetContent, } from "@/components/ui/sheet" -import { getComboBoxOptions } from "@/lib/docu-list-rule/combo-box-settings/service" +import { getComboBoxOptions, updateComboBoxOption } 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" @@ -21,6 +25,7 @@ type ComboBoxOption = { code: string description: string remark: string | null + sdq: number isActive: boolean createdAt: Date updatedAt: Date @@ -57,7 +62,7 @@ export function ComboBoxOptionsDetailSheet({ page: 1, perPage: 10, search: "", - sort: [{ id: "createdAt", desc: true }], + sort: [{ id: "sdq", desc: false }], filters: [], joinOperator: "and", }) @@ -90,6 +95,7 @@ export function ComboBoxOptionsDetailSheet({ const result = await getComboBoxOptions(codeGroup.id, { page: 1, perPage: 10, + sort: [{ id: "sdq", desc: false }], }) if (result.success && result.data) { const optionsWithIsActive = result.data.map(option => ({ @@ -123,7 +129,7 @@ export function ComboBoxOptionsDetailSheet({ getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), initialState: { - sorting: [{ id: "code", desc: false }], + sorting: [{ id: "sdq", desc: false }], pagination: { pageSize: 10, }, @@ -131,6 +137,66 @@ export function ComboBoxOptionsDetailSheet({ getRowId: (originalRow) => String((originalRow as any).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 })) + + // 서버에 순서 업데이트 (Number Type Config와 같은 방식) + try { + // 모든 항목을 임시 값으로 먼저 업데이트 + for (let i = 0; i < updatedOptions.length; i++) { + const option = updatedOptions[i] + await updateComboBoxOption({ + id: option.id, + codeGroupId: option.codeGroupId, + code: option.code, + description: option.description, + remark: option.remark, + sdq: -(i + 1), // 임시 음수 값 + }) + } + + // 최종 순서로 업데이트 + for (const option of updatedOptions) { + await updateComboBoxOption({ + id: option.id, + codeGroupId: option.codeGroupId, + code: option.code, + description: option.description, + remark: option.remark, + sdq: option.sdq, + }) + } + + toast.success("순서가 성공적으로 변경되었습니다.") + } catch (error) { + console.error("Error updating order:", error) + toast.error("순서 변경 중 오류가 발생했습니다.") + // 에러 시 원래 데이터로 복원 + await refreshData() + } + } + } + }, [rawData.data, codeGroup, refreshData]) + if (!codeGroup) return null return ( @@ -151,12 +217,16 @@ export function ComboBoxOptionsDetailSheet({ onSuccess={refreshData} /> - <DataTableDetail table={table as any}> + <DragDropTable + table={table as any} + data={rawData.data} + onDragEnd={handleDragEnd} + > <DataTableAdvancedToolbarDetail table={table as any} filterFields={advancedFilterFields} /> - </DataTableDetail> + </DragDropTable> <DeleteComboBoxOptionsDialog open={rowAction?.type === "delete"} 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 17754331..cf770c33 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 @@ -25,6 +25,7 @@ interface ComboBoxOption { code: string description: string remark: string | null + sdq: number isActive?: boolean createdAt: Date updatedAt: Date @@ -114,17 +115,17 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<ComboBo // ---------------------------------------------------------------- const dataColumns: ColumnDef<ComboBoxOption>[] = [ { - accessorKey: "projectCode", + accessorKey: "sdq", enableResizing: true, header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="프로젝트 코드" /> + <DataTableColumnHeaderSimple column={column} title="순서" /> ), meta: { - excelHeader: "프로젝트 코드", - type: "text", + excelHeader: "순서", + type: "number", }, - cell: ({ row }) => row.getValue("projectCode") ?? "", - minSize: 120 + cell: ({ row }) => row.getValue("sdq") ?? "", + minSize: 50 }, { accessorKey: "code", diff --git a/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table-columns.tsx b/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table-columns.tsx index d41fe5ec..0775c1d2 100644 --- a/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table-columns.tsx +++ b/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table-columns.tsx @@ -91,19 +91,7 @@ export function getColumns({ onDetail }: GetColumnsProps): ColumnDef<typeof code // 3) 데이터 컬럼들 // ---------------------------------------------------------------- const dataColumns: ColumnDef<typeof codeGroups.$inferSelect>[] = [ - { - accessorKey: "projectCode", - enableResizing: true, - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="프로젝트 코드" /> - ), - meta: { - excelHeader: "프로젝트 코드", - type: "text", - }, - cell: ({ row }) => row.getValue("projectCode") ?? "", - minSize: 120 - }, + { accessorKey: "groupId", enableResizing: true, 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 8e469149..fef4cf8e 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 @@ -66,12 +66,7 @@ export function ComboBoxSettingsTable({ promises }: ComboBoxSettingsTableProps) clearOnDefault: true, }) - // 컴포넌트 마운트 후 그룹핑 설정 - 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/combo-box-settings/validation.ts b/lib/docu-list-rule/combo-box-settings/validation.ts index ca8e9192..51671fb2 100644 --- a/lib/docu-list-rule/combo-box-settings/validation.ts +++ b/lib/docu-list-rule/combo-box-settings/validation.ts @@ -24,6 +24,7 @@ export const searchParamsComboBoxSettingsCache = createSearchParamsCache({ description: parseAsString.withDefault(""), controlType: parseAsString.withDefault(""), isActive: parseAsString.withDefault(""), + projectId: parseAsString.withDefault(""), // advanced filter filters: getFiltersStateParser().withDefault([]), 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 diff --git a/lib/docu-list-rule/number-type-configs/service.ts b/lib/docu-list-rule/number-type-configs/service.ts index 01e045ed..f4cadc70 100644 --- a/lib/docu-list-rule/number-type-configs/service.ts +++ b/lib/docu-list-rule/number-type-configs/service.ts @@ -3,7 +3,7 @@ import { revalidatePath } from "next/cache" import db from "@/db/db" import { unstable_noStore } from "next/cache" -import { documentNumberTypeConfigs, codeGroups } from "@/db/schema/docu-list-rule" +import { documentNumberTypeConfigs, codeGroups, documentNumberTypes } from "@/db/schema/docu-list-rule" import { projects } from "@/db/schema/projects" import { asc, eq, sql, and } from "drizzle-orm" import { GetNumberTypeConfigsSchema } from "./validation" @@ -27,6 +27,11 @@ export async function getNumberTypeConfigs(input: GetNumberTypeConfigsSchema) { // 기본 조건: 특정 Number Type let whereConditions = eq(documentNumberTypeConfigs.documentNumberTypeId, numberTypeId) + + // 프로젝트 ID 필터링 (Number Type이 해당 프로젝트에 속하는지 확인) + if (input.projectId) { + whereConditions = sql`${whereConditions} AND ${documentNumberTypes.projectId} = ${parseInt(input.projectId)}` + } // 검색 조건 추가 if (search && search.trim() !== "") { @@ -128,6 +133,7 @@ export async function getNumberTypeConfigs(input: GetNumberTypeConfigsSchema) { .from(documentNumberTypeConfigs) .leftJoin(codeGroups, eq(documentNumberTypeConfigs.codeGroupId, codeGroups.id)) .leftJoin(projects, eq(codeGroups.projectId, projects.id)) + .leftJoin(documentNumberTypes, eq(documentNumberTypeConfigs.documentNumberTypeId, documentNumberTypes.id)) .where(whereConditions) .orderBy(orderBy) .limit(perPage) @@ -139,6 +145,7 @@ export async function getNumberTypeConfigs(input: GetNumberTypeConfigsSchema) { .from(documentNumberTypeConfigs) .leftJoin(codeGroups, eq(documentNumberTypeConfigs.codeGroupId, codeGroups.id)) .leftJoin(projects, eq(codeGroups.projectId, projects.id)) + .leftJoin(documentNumberTypes, eq(documentNumberTypeConfigs.documentNumberTypeId, documentNumberTypes.id)) .where(whereConditions) const totalCount = totalCountResult[0]?.count || 0 diff --git a/lib/docu-list-rule/number-type-configs/table/number-type-configs-table-columns.tsx b/lib/docu-list-rule/number-type-configs/table/number-type-configs-table-columns.tsx index 24255acf..260c50cd 100644 --- a/lib/docu-list-rule/number-type-configs/table/number-type-configs-table-columns.tsx +++ b/lib/docu-list-rule/number-type-configs/table/number-type-configs-table-columns.tsx @@ -49,19 +49,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<NumberT enableHiding: false, size: 35, }, - { - accessorKey: "projectCode", - enableResizing: true, - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="프로젝트 코드" /> - ), - meta: { - excelHeader: "프로젝트 코드", - type: "text", - }, - cell: ({ row }) => row.getValue("projectCode") ?? "", - minSize: 120 - }, + { accessorKey: "sdq", enableResizing: true, diff --git a/lib/docu-list-rule/number-type-configs/table/number-type-configs-table.tsx b/lib/docu-list-rule/number-type-configs/table/number-type-configs-table.tsx index 1bddfa52..df01fd81 100644 --- a/lib/docu-list-rule/number-type-configs/table/number-type-configs-table.tsx +++ b/lib/docu-list-rule/number-type-configs/table/number-type-configs-table.tsx @@ -7,8 +7,9 @@ import type { DataTableAdvancedFilterField, DataTableRowAction } from "@/types/t import { arrayMove } from '@dnd-kit/sortable' import { DragEndEvent } from '@dnd-kit/core' import { toast } from "sonner" +import { useParams } from "next/navigation" -import { getNumberTypeConfigs, updateNumberTypeConfig, updateNumberTypeConfigOrder } from "@/lib/docu-list-rule/number-type-configs/service" +import { getNumberTypeConfigs, updateNumberTypeConfig } from "@/lib/docu-list-rule/number-type-configs/service" import { getColumns } from "@/lib/docu-list-rule/number-type-configs/table/number-type-configs-table-columns" import { DeleteNumberTypeConfigsDialog } from "@/lib/docu-list-rule/number-type-configs/table/delete-number-type-configs-dialog" import { NumberTypeConfigsEditDialog } from "@/lib/docu-list-rule/number-type-configs/table/number-type-configs-edit-dialog" @@ -32,7 +33,8 @@ interface NumberTypeConfigsTableProps { export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeConfigsTableProps) { const rawData = React.use(promises!) - const [selectedProjectId, setSelectedProjectId] = React.useState<number | null>(null) + const params = useParams() + const projectId = Number(params?.projectId) const [selectedNumberType, setSelectedNumberType] = React.useState<number | null>(null) const [configsData, setConfigsData] = React.useState<{ data: NumberTypeConfig[]; pageCount: number }>({ data: [], pageCount: 0 }) const [rowAction, setRowAction] = React.useState<DataTableRowAction<NumberTypeConfig> | null>(null) @@ -64,18 +66,12 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon const result = rawData[0] if (result.data && result.data.length > 0 && isInitialLoad.current) { - // 초기 로드 시 첫 번째 프로젝트의 첫 번째 Number Type을 선택 - const firstProjectId = result.data[0].projectId - setSelectedProjectId(firstProjectId) + // 초기 로드 시 첫 번째 Number Type을 선택 + const firstNumberType = result.data[0] + setSelectedNumberType(firstNumberType.id) - const firstProjectNumberTypes = result.data.filter(nt => nt.projectId === firstProjectId) - if (firstProjectNumberTypes.length > 0) { - const firstNumberType = firstProjectNumberTypes[0] - setSelectedNumberType(firstNumberType.id) - - // 첫 번째 타입의 configs 데이터도 바로 로드 - await fetchConfigs(firstNumberType.id) - } + // 첫 번째 타입의 configs 데이터도 바로 로드 + await fetchConfigs(firstNumberType.id) // 초기 로드 완료 표시 isInitialLoad.current = false @@ -89,25 +85,7 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon loadData() }, [rawData, fetchConfigs]) - // 프로젝트 변경 핸들러 - const handleProjectChange = React.useCallback((projectId: number | null) => { - setSelectedProjectId(projectId) - setConfigsData({ data: [], pageCount: 0 }) // Configs 데이터 초기화 - - if (projectId && rawData[0]?.data) { - // 선택된 프로젝트의 첫 번째 Number Type을 자동 선택 - const selectedProjectNumberTypes = rawData[0].data.filter(nt => nt.projectId === projectId) - if (selectedProjectNumberTypes.length > 0) { - const firstNumberType = selectedProjectNumberTypes[0] - setSelectedNumberType(firstNumberType.id) - fetchConfigs(firstNumberType.id) - } else { - setSelectedNumberType(null) - } - } else { - setSelectedNumberType(null) - } - }, [rawData, fetchConfigs]) + const handleNumberTypeChange = React.useCallback((numberTypeId: number) => { @@ -253,8 +231,6 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon numberTypes={rawData[0]?.data || []} selectedNumberType={selectedNumberType} onNumberTypeChange={handleNumberTypeChange} - selectedProjectId={selectedProjectId} - onProjectChange={handleProjectChange} isLoading={!rawData[0]?.data} /> @@ -273,7 +249,7 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon table={table} onSuccess={refreshData} selectedNumberType={selectedNumberType} - selectedProjectId={selectedProjectId} + selectedProjectId={projectId} configsData={configsData.data} /> </DataTableAdvancedToolbar> @@ -297,7 +273,6 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon onOpenChange={() => setRowAction(null)} data={rowAction?.row.original ?? null} existingConfigs={configsData.data} - selectedProjectId={selectedProjectId} onSuccess={() => { setRowAction(null) refreshData() diff --git a/lib/docu-list-rule/number-type-configs/table/number-type-selector.tsx b/lib/docu-list-rule/number-type-configs/table/number-type-selector.tsx index d01e25d5..dd8aa023 100644 --- a/lib/docu-list-rule/number-type-configs/table/number-type-selector.tsx +++ b/lib/docu-list-rule/number-type-configs/table/number-type-selector.tsx @@ -2,13 +2,7 @@ import * as React from "react" import { documentNumberTypes } from "@/db/schema/docu-list-rule" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" + // Number Type with project info type type NumberTypeWithProject = typeof documentNumberTypes.$inferSelect & { @@ -20,8 +14,6 @@ interface NumberTypeSelectorProps { numberTypes: NumberTypeWithProject[] selectedNumberType: number | null onNumberTypeChange: (numberTypeId: number) => void - selectedProjectId: number | null - onProjectChange: (projectId: number | null) => void isLoading?: boolean } @@ -29,60 +21,14 @@ export function NumberTypeSelector({ numberTypes, selectedNumberType, onNumberTypeChange, - selectedProjectId, - onProjectChange, isLoading = false }: NumberTypeSelectorProps) { - // 프로젝트별로 Number Types 그룹화 - const projectGroups = React.useMemo(() => { - const groups: { [key: string]: { id: number; code: string; name: string; numberTypes: NumberTypeWithProject[] } } = {} - - numberTypes.forEach(numberType => { - const projectKey = `${numberType.projectId}` - if (!groups[projectKey]) { - groups[projectKey] = { - id: numberType.projectId, - code: numberType.projectCode || '', - name: numberType.projectName || '', - numberTypes: [] - } - } - groups[projectKey].numberTypes.push(numberType) - }) - - return Object.values(groups).sort((a, b) => a.code.localeCompare(b.code)) - }, [numberTypes]) - - // 선택된 프로젝트의 Number Types만 필터링 - const filteredNumberTypes = React.useMemo(() => { - if (!selectedProjectId) return [] - return numberTypes.filter(nt => nt.projectId === selectedProjectId) - }, [numberTypes, selectedProjectId]) - - // 프로젝트 변경 시 Number Type 선택 초기화 - const handleNumberTypeReset = React.useCallback(() => { - if (selectedProjectId && selectedNumberType) { - const isNumberTypeInProject = filteredNumberTypes.some(nt => nt.id === selectedNumberType) - if (!isNumberTypeInProject) { - onNumberTypeChange(0) // 선택 해제 - } - } - }, [selectedProjectId, selectedNumberType, filteredNumberTypes, onNumberTypeChange]) - React.useEffect(() => { - handleNumberTypeReset() - }, [handleNumberTypeReset]) if (isLoading) { return ( <div className="mb-6 space-y-4"> <div> - <label className="text-sm font-medium mb-2 block">프로젝트 선택</label> - <div className="px-4 py-2 text-sm text-muted-foreground"> - 프로젝트를 불러오는 중... - </div> - </div> - <div> <label className="text-sm font-medium mb-2 block">Number Type 선택</label> <div className="px-4 py-2 text-sm text-muted-foreground"> Number Type을 불러오는 중... @@ -96,12 +42,6 @@ export function NumberTypeSelector({ return ( <div className="mb-6 space-y-4"> <div> - <label className="text-sm font-medium mb-2 block">프로젝트 선택</label> - <div className="px-4 py-2 text-sm text-muted-foreground"> - 사용 가능한 프로젝트가 없습니다. - </div> - </div> - <div> <label className="text-sm font-medium mb-2 block">Number Type 선택</label> <div className="px-4 py-2 text-sm text-muted-foreground"> 사용 가능한 Number Type이 없습니다. @@ -113,54 +53,24 @@ export function NumberTypeSelector({ return ( <div className="mb-6 space-y-4"> - {/* 프로젝트 선택 */} - <div> - <label className="text-sm font-medium mb-2 block">프로젝트 선택</label> - <Select - value={selectedProjectId?.toString() || ""} - onValueChange={(value) => onProjectChange(value ? parseInt(value) : null)} - > - <SelectTrigger className="w-[300px]"> - <SelectValue placeholder="프로젝트를 선택하세요" /> - </SelectTrigger> - <SelectContent> - {projectGroups.map((project) => ( - <SelectItem key={project.id} value={project.id.toString()}> - {project.code} - {project.name} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - {/* Number Type 선택 */} <div> <label className="text-sm font-medium mb-2 block">Number Type 선택</label> - {!selectedProjectId ? ( - <div className="px-4 py-2 text-sm text-muted-foreground"> - 프로젝트를 먼저 선택해주세요. - </div> - ) : filteredNumberTypes.length === 0 ? ( - <div className="px-4 py-2 text-sm text-muted-foreground"> - 선택한 프로젝트에 Number Type이 없습니다. - </div> - ) : ( - <div className="flex gap-2 flex-wrap"> - {filteredNumberTypes.map((numberType) => ( - <button - key={numberType.id} - onClick={() => onNumberTypeChange(numberType.id)} - className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${ - selectedNumberType === numberType.id - ? "bg-primary text-primary-foreground" - : "bg-muted text-muted-foreground hover:bg-muted/80" - }`} - > - {numberType.name} - </button> - ))} - </div> - )} + <div className="flex gap-2 flex-wrap"> + {numberTypes.map((numberType) => ( + <button + key={numberType.id} + onClick={() => onNumberTypeChange(numberType.id)} + className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${ + selectedNumberType === numberType.id + ? "bg-primary text-primary-foreground" + : "bg-muted text-muted-foreground hover:bg-muted/80" + }`} + > + {numberType.name} + </button> + ))} + </div> </div> </div> ) diff --git a/lib/docu-list-rule/number-type-configs/validation.ts b/lib/docu-list-rule/number-type-configs/validation.ts index deb8193a..5b7977b4 100644 --- a/lib/docu-list-rule/number-type-configs/validation.ts +++ b/lib/docu-list-rule/number-type-configs/validation.ts @@ -28,6 +28,7 @@ export const searchParamsNumberTypeConfigsCache = createSearchParamsCache({ description: parseAsString.withDefault(""), remark: parseAsString.withDefault(""), isActive: parseAsString.withDefault(""), + projectId: parseAsString.withDefault(""), // 고급 필터 filters: getFiltersStateParser().withDefault([]), diff --git a/lib/docu-list-rule/number-types/service.ts b/lib/docu-list-rule/number-types/service.ts index 18c5e6a3..3fb51abd 100644 --- a/lib/docu-list-rule/number-types/service.ts +++ b/lib/docu-list-rule/number-types/service.ts @@ -19,6 +19,7 @@ export async function getNumberTypes(input: { numberTypeId?: string description?: string isActive?: string + projectId?: string }) { unstable_noStore() @@ -29,10 +30,15 @@ export async function getNumberTypes(input: { // 기본 조건 (plant 타입 프로젝트만) let whereConditions = sql`${projects.type} = 'plant'` + // 프로젝트 ID 필터링 + if (input.projectId) { + whereConditions = sql`${whereConditions} AND ${documentNumberTypes.projectId} = ${parseInt(input.projectId)}` + } + // 검색 조건 if (search) { const searchTerm = `%${search}%` - whereConditions = sql`${projects.type} = 'plant' AND ( + whereConditions = sql`${whereConditions} AND ( ${documentNumberTypes.name} ILIKE ${searchTerm} OR ${documentNumberTypes.description} ILIKE ${searchTerm} OR ${projects.code} ILIKE ${searchTerm} @@ -373,6 +379,7 @@ export async function getNumberTypesWithConfigs(input: { numberTypeId?: string description?: string isActive?: string + projectId?: string }) { unstable_noStore() @@ -383,10 +390,15 @@ export async function getNumberTypesWithConfigs(input: { // 기본 조건 (plant 타입 프로젝트만) let whereConditions = sql`${projects.type} = 'plant'` + // 프로젝트 ID 필터링 + if (input.projectId) { + whereConditions = sql`${whereConditions} AND ${documentNumberTypes.projectId} = ${parseInt(input.projectId)}` + } + // 검색 조건 if (search) { const searchTerm = `%${search}%` - whereConditions = sql`${projects.type} = 'plant' AND ( + whereConditions = sql`${whereConditions} AND ( ${documentNumberTypes.name} ILIKE ${searchTerm} OR ${documentNumberTypes.description} ILIKE ${searchTerm} OR ${projects.code} ILIKE ${searchTerm} diff --git a/lib/docu-list-rule/number-types/table/number-type-add-dialog.tsx b/lib/docu-list-rule/number-types/table/number-type-add-dialog.tsx index 903cd5c6..545e3965 100644 --- a/lib/docu-list-rule/number-types/table/number-type-add-dialog.tsx +++ b/lib/docu-list-rule/number-types/table/number-type-add-dialog.tsx @@ -14,70 +14,30 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" import { toast } from "sonner" import { createNumberType } from "@/lib/docu-list-rule/number-types/service" -import { getProjectLists } from "@/lib/projects/service" +import { useParams } from "next/navigation" interface NumberTypeAddDialogProps { onSuccess: () => void } export function NumberTypeAddDialog({ onSuccess }: NumberTypeAddDialogProps) { + const params = useParams() + const projectId = Number(params?.projectId) const [open, setOpen] = React.useState(false) const [loading, setLoading] = React.useState(false) - const [projects, setProjects] = React.useState<Array<{ id: number; code: string; name: string; type: string }>>([]) + const [formData, setFormData] = React.useState({ - projectId: "", name: "", description: "", }) - // 프로젝트 목록 로드 - React.useEffect(() => { - if (open) { - const loadProjects = async () => { - try { - const result = await getProjectLists({ - name: "", - search: "", - code: "", - type: "", - sort: [], - filters: [], - joinOperator: "and", - flags: [], - page: 1, - perPage: 1000 - }) - 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]) + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() - if (!formData.projectId) { - toast.error("프로젝트는 필수 선택 항목입니다.") - return - } - if (!formData.name.trim()) { toast.error("Name은 필수 입력 항목입니다.") return @@ -86,14 +46,14 @@ export function NumberTypeAddDialog({ onSuccess }: NumberTypeAddDialogProps) { setLoading(true) try { const result = await createNumberType({ - projectId: parseInt(formData.projectId), + projectId: projectId, name: formData.name.trim(), description: formData.description.trim() || undefined, }) if (result.success) { toast.success("Number Type이 생성되었습니다.") - setFormData({ projectId: "", name: "", description: "" }) + setFormData({ name: "", description: "" }) setOpen(false) onSuccess() } else { @@ -108,7 +68,7 @@ export function NumberTypeAddDialog({ onSuccess }: NumberTypeAddDialogProps) { } const handleCancel = () => { - setFormData({ projectId: "", name: "", description: "" }) + setFormData({ name: "", description: "" }) setOpen(false) } @@ -130,21 +90,6 @@ export function NumberTypeAddDialog({ onSuccess }: NumberTypeAddDialogProps) { </DialogHeader> <form onSubmit={handleSubmit} className="space-y-4"> <div className="space-y-2"> - <Label htmlFor="projectId">프로젝트 *</Label> - <Select onValueChange={(value) => setFormData(prev => ({ ...prev, projectId: value }))} value={formData.projectId}> - <SelectTrigger> - <SelectValue placeholder="프로젝트를 선택하세요" /> - </SelectTrigger> - <SelectContent> - {projects.map((project) => ( - <SelectItem key={project.id} value={project.id.toString()}> - {project.code} - {project.name} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - <div className="space-y-2"> <Label htmlFor="name">Name *</Label> <Input id="name" @@ -175,7 +120,7 @@ export function NumberTypeAddDialog({ onSuccess }: NumberTypeAddDialogProps) { </Button> <Button type="submit" - disabled={loading || !formData.projectId || !formData.name.trim()} + disabled={loading || !formData.name.trim()} > {loading ? "Creating..." : "Create"} </Button> diff --git a/lib/docu-list-rule/number-types/table/number-types-table-columns.tsx b/lib/docu-list-rule/number-types/table/number-types-table-columns.tsx index c529bcfb..59403f3d 100644 --- a/lib/docu-list-rule/number-types/table/number-types-table-columns.tsx +++ b/lib/docu-list-rule/number-types/table/number-types-table-columns.tsx @@ -109,19 +109,6 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<NumberT // ---------------------------------------------------------------- const dataColumns: ColumnDef<NumberTypeWithConfigs>[] = [ { - accessorKey: "projectCode", - enableResizing: true, - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="프로젝트 코드" /> - ), - meta: { - excelHeader: "프로젝트 코드", - type: "text", - }, - cell: ({ row }) => row.getValue("projectCode") ?? "", - minSize: 120 - }, - { accessorKey: "name", enableResizing: true, header: ({ column }) => ( diff --git a/lib/docu-list-rule/number-types/table/number-types-table.tsx b/lib/docu-list-rule/number-types/table/number-types-table.tsx index 22e63363..a57d0634 100644 --- a/lib/docu-list-rule/number-types/table/number-types-table.tsx +++ b/lib/docu-list-rule/number-types/table/number-types-table.tsx @@ -63,12 +63,7 @@ export function NumberTypesTable({ promises }: NumberTypesTableProps) { clearOnDefault: true, }) - // 컴포넌트 마운트 후 그룹핑 설정 - 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/number-types/validation.ts b/lib/docu-list-rule/number-types/validation.ts index 6382ee5a..9d281b1b 100644 --- a/lib/docu-list-rule/number-types/validation.ts +++ b/lib/docu-list-rule/number-types/validation.ts @@ -9,4 +9,5 @@ export const searchParamsNumberTypesCache = createSearchParamsCache({ filters: getFiltersStateParser(), search: parseAsString.withDefault(""), joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + projectId: parseAsString.withDefault(""), });
\ No newline at end of file |
