diff options
Diffstat (limited to 'lib')
38 files changed, 813 insertions, 199 deletions
diff --git a/lib/docu-list-rule/code-groups/service.ts b/lib/docu-list-rule/code-groups/service.ts index c99588a5..843bafa2 100644 --- a/lib/docu-list-rule/code-groups/service.ts +++ b/lib/docu-list-rule/code-groups/service.ts @@ -3,7 +3,8 @@ import { revalidatePath } from "next/cache" import db from "@/db/db" import { codeGroups, comboBoxSettings } from "@/db/schema/docu-list-rule" -import { eq, sql, count } from "drizzle-orm" +import { projects } from "@/db/schema/projects" +import { eq, sql, count, and } from "drizzle-orm" import { unstable_noStore } from "next/cache" // Code Groups 목록 조회 @@ -24,7 +25,8 @@ export async function getCodeGroups(input: any) { ${codeGroups.groupId} ILIKE ${searchTerm} OR ${codeGroups.description} ILIKE ${searchTerm} OR ${codeGroups.codeFormat} ILIKE ${searchTerm} OR - ${codeGroups.controlType} ILIKE ${searchTerm} + ${codeGroups.controlType} ILIKE ${searchTerm} OR + ${projects.code} ILIKE ${searchTerm} )` } @@ -68,14 +70,20 @@ export async function getCodeGroups(input: any) { if (input.sort && input.sort.length > 0) { const sortField = input.sort[0] // 안전성 체크: 필드가 실제 테이블에 존재하는지 확인 - if (sortField && sortField.id && typeof sortField.id === "string" && sortField.id in codeGroups) { + if (sortField && sortField.id && typeof sortField.id === "string") { const direction = sortField.desc ? sql`DESC` : sql`ASC` - const col = codeGroups[sortField.id as keyof typeof codeGroups] - orderBy = sql`${col} ${direction}` + + // 프로젝트 코드 정렬 처리 + if (sortField.id === "projectCode") { + orderBy = sql`${projects.code} ${direction}` + } else if (sortField.id in codeGroups) { + const col = codeGroups[sortField.id as keyof typeof codeGroups] + orderBy = sql`${col} ${direction}` + } } } - // 데이터 조회 + // 데이터 조회 (프로젝트 정보 포함) const data = await db .select({ id: codeGroups.id, @@ -87,38 +95,44 @@ export async function getCodeGroups(input: any) { isActive: codeGroups.isActive, createdAt: codeGroups.createdAt, updatedAt: codeGroups.updatedAt, + projectId: codeGroups.projectId, + projectCode: projects.code, + projectName: projects.name, }) .from(codeGroups) + .leftJoin(projects, eq(codeGroups.projectId, projects.id)) .where(whereConditions) .orderBy(orderBy) .limit(perPage) .offset(offset) - // 총 개수 조회 (Document Class 제외) - const [{ count: total }] = await db - .select({ count: count() }) + // 총 개수 조회 (프로젝트 정보 포함) + const totalCountResult = await db + .select({ count: sql<number>`count(*)` }) .from(codeGroups) + .leftJoin(projects, eq(codeGroups.projectId, projects.id)) .where(whereConditions) - const pageCount = Math.ceil(total / perPage) + const totalCount = totalCountResult[0]?.count || 0 return { data, - pageCount, - total, + totalCount, + pageCount: Math.ceil(totalCount / perPage), } } catch (error) { console.error("Error fetching code groups:", error) return { data: [], + totalCount: 0, pageCount: 0, - total: 0, } } } // Code Group 생성 export async function createCodeGroup(input: { + projectId: number // projectCode를 projectId로 변경 description: string codeFormat?: string expressions?: string @@ -126,11 +140,14 @@ export async function createCodeGroup(input: { isActive?: boolean }) { try { - // 마지막 Code Group의 groupId를 찾아서 다음 번호 생성 (DOC_CLASS 제외) + // 해당 프로젝트의 마지막 Code Group의 groupId를 찾아서 다음 번호 생성 (DOC_CLASS 제외) const lastCodeGroup = await db .select({ groupId: codeGroups.groupId }) .from(codeGroups) - .where(sql`${codeGroups.groupId} != 'DOC_CLASS'`) + .where(and( + eq(codeGroups.projectId, input.projectId), // projectId로 변경 + sql`${codeGroups.groupId} != 'DOC_CLASS'` + )) .orderBy(sql`CAST(SUBSTRING(${codeGroups.groupId}, 6) AS INTEGER) DESC`) .limit(1) @@ -148,6 +165,7 @@ export async function createCodeGroup(input: { const [newCodeGroup] = await db .insert(codeGroups) .values({ + projectId: input.projectId, // projectId로 변경 groupId: newGroupId, description: input.description, codeFormat: input.codeFormat, 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 bf044f1a..a0143239 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 @@ -33,10 +33,12 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" -import { createCodeGroup } from "../service" +import { createCodeGroup } from "@/lib/docu-list-rule/code-groups/service" +import { getProjectLists } from "@/lib/projects/service" 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,16 +56,44 @@ interface CodeGroupsAddDialogProps { export function CodeGroupsAddDialog({ onSuccess }: CodeGroupsAddDialogProps) { const [open, setOpen] = React.useState(false) const [isLoading, setIsLoading] = React.useState(false) + const [projects, setProjects] = React.useState<Array<{ id: number; code: string; name: 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: [] + }) + if (result.data) { + setProjects(result.data) + } + } catch (error) { + console.error("Failed to load projects:", error) + toast.error("프로젝트 목록을 불러오는데 실패했습니다.") + } + } + loadProjects() + } + }, [open]) + // Code Format을 기반으로 정규식 자동 생성 함수 const generateExpression = (codeFormat: string): string => { if (!codeFormat) return '' @@ -121,6 +151,7 @@ export function CodeGroupsAddDialog({ onSuccess }: CodeGroupsAddDialogProps) { const expressions = generateExpression(data.codeFormat || "") const result = await createCodeGroup({ + projectId: parseInt(data.projectId), description: data.description, codeFormat: data.codeFormat, expressions: expressions, @@ -156,6 +187,7 @@ export function CodeGroupsAddDialog({ onSuccess }: CodeGroupsAddDialogProps) { <DialogTitle>Code Group 생성</DialogTitle> <DialogDescription> 새로운 Code Group을 생성합니다. + <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span> </DialogDescription> </DialogHeader> @@ -163,10 +195,35 @@ 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> - <FormLabel>Description</FormLabel> + <FormLabel>Description *</FormLabel> <FormControl> <Input placeholder="예: PROJECT NO" {...field} /> </FormControl> diff --git a/lib/docu-list-rule/code-groups/table/code-groups-edit-sheet.tsx b/lib/docu-list-rule/code-groups/table/code-groups-edit-sheet.tsx index 74ebc05a..7dc714a7 100644 --- a/lib/docu-list-rule/code-groups/table/code-groups-edit-sheet.tsx +++ b/lib/docu-list-rule/code-groups/table/code-groups-edit-sheet.tsx @@ -34,8 +34,8 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" -import { updateCodeGroup } from "../service" -import { codeGroups } from "@/db/schema/codeGroups" +import { updateCodeGroup } from "@/lib/docu-list-rule/code-groups/service" +import { codeGroups } from "@/db/schema/docu-list-rule" import { z } from "zod" const updateCodeGroupSchema = z.object({ 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 c15dd676..01047c50 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,6 +103,19 @@ 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-toolbar.tsx b/lib/docu-list-rule/code-groups/table/code-groups-table-toolbar.tsx index d2d9efb4..4dc3334c 100644 --- a/lib/docu-list-rule/code-groups/table/code-groups-table-toolbar.tsx +++ b/lib/docu-list-rule/code-groups/table/code-groups-table-toolbar.tsx @@ -3,9 +3,9 @@ import * as React from "react" import { type Table } from "@tanstack/react-table" -import { DeleteCodeGroupsDialog } from "./delete-code-groups-dialog" -import { CodeGroupsAddDialog } from "./code-groups-add-dialog" -import { codeGroups } from "@/db/schema/codeGroups" +import { DeleteCodeGroupsDialog } from "@/lib/docu-list-rule/code-groups/table/delete-code-groups-dialog" +import { CodeGroupsAddDialog } from "@/lib/docu-list-rule/code-groups/table/code-groups-add-dialog" +import { codeGroups } from "@/db/schema/docu-list-rule" interface CodeGroupsTableToolbarActionsProps<TData> { table: Table<TData> 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 8873c34c..0029ed91 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 @@ -9,10 +9,10 @@ import type { DataTableFilterField, DataTableRowAction, } from "@/types/table" -import { getColumns } from "./code-groups-table-columns"; -import { DeleteCodeGroupsDialog } from "./delete-code-groups-dialog"; -import { CodeGroupsEditSheet } from "./code-groups-edit-sheet"; -import { CodeGroupsTableToolbarActions } from "./code-groups-table-toolbar"; +import { getColumns } from "@/lib/docu-list-rule/code-groups/table/code-groups-table-columns"; +import { DeleteCodeGroupsDialog } from "@/lib/docu-list-rule/code-groups/table/delete-code-groups-dialog"; +import { CodeGroupsEditSheet } from "@/lib/docu-list-rule/code-groups/table/code-groups-edit-sheet"; +import { CodeGroupsTableToolbarActions } from "@/lib/docu-list-rule/code-groups/table/code-groups-table-toolbar"; import { codeGroups } from "@/db/schema/docu-list-rule"; interface CodeGroupsTableProps { @@ -68,12 +68,34 @@ export function CodeGroupsTable({ promises }: CodeGroupsTableProps) { enableAdvancedFilter: true, initialState: { columnPinning: { right: ["actions"] }, + expanded: {}, }, - getRowId: (originalRow) => String(originalRow.groupId), + getRowId: (originalRow) => String(originalRow.id), shallow: false, - clearOnDefault: true, + clearOnDefault: false, }) + // 컴포넌트 마운트 후 그룹핑 설정 + React.useEffect(() => { + if (data && table.getState().grouping.length === 0) { + table.setGrouping(["projectCode"]) + } + }, [table, data]) + + // 정렬 시 펼쳐진 상태 유지 + React.useEffect(() => { + const currentExpanded = table.getState().expanded + if (Object.keys(currentExpanded).length > 0) { + // 약간의 지연 후 현재 펼쳐진 상태를 다시 설정 + const timer = setTimeout(() => { + table.setExpanded(currentExpanded) + }, 100) + return () => clearTimeout(timer) + } + }, [table.getState().sorting, table]) + + + return ( <> <DataTable table={table}> diff --git a/lib/docu-list-rule/code-groups/table/delete-code-groups-dialog.tsx b/lib/docu-list-rule/code-groups/table/delete-code-groups-dialog.tsx index 66a8d7c2..6f2217bd 100644 --- a/lib/docu-list-rule/code-groups/table/delete-code-groups-dialog.tsx +++ b/lib/docu-list-rule/code-groups/table/delete-code-groups-dialog.tsx @@ -27,8 +27,8 @@ import { DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer" -import { codeGroups } from "@/db/schema/codeGroups" -import { deleteCodeGroup } from "../service" +import { codeGroups } from "@/db/schema/docu-list-rule" +import { deleteCodeGroup } from "@/lib/docu-list-rule/code-groups/service" interface DeleteCodeGroupsDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> { diff --git a/lib/docu-list-rule/combo-box-settings/service.ts b/lib/docu-list-rule/combo-box-settings/service.ts index 80c1942d..96daefe4 100644 --- a/lib/docu-list-rule/combo-box-settings/service.ts +++ b/lib/docu-list-rule/combo-box-settings/service.ts @@ -3,6 +3,7 @@ import { revalidatePath } from "next/cache" import db from "@/db/db" import { codeGroups, comboBoxSettings } from "@/db/schema/docu-list-rule" +import { projects } from "@/db/schema/projects" import { eq, sql, count } from "drizzle-orm" import { unstable_noStore } from "next/cache" @@ -34,7 +35,8 @@ export async function getComboBoxCodeGroups(input: { whereConditions = sql`${whereConditions} AND ( ${codeGroups.groupId} ILIKE ${searchTerm} OR ${codeGroups.description} ILIKE ${searchTerm} OR - ${codeGroups.codeFormat} ILIKE ${searchTerm} + ${codeGroups.codeFormat} ILIKE ${searchTerm} OR + ${projects.code} ILIKE ${searchTerm} )` } @@ -78,14 +80,20 @@ export async function getComboBoxCodeGroups(input: { if (sort && sort.length > 0) { const sortField = sort[0] // 안전성 체크: 필드가 실제 테이블에 존재하는지 확인 - if (sortField && sortField.id && typeof sortField.id === "string" && sortField.id in codeGroups) { + if (sortField && sortField.id && typeof sortField.id === "string") { const direction = sortField.desc ? sql`DESC` : sql`ASC` - const col = codeGroups[sortField.id as keyof typeof codeGroups] - orderBy = sql`${col} ${direction}` + + // 프로젝트 코드 정렬 처리 + if (sortField.id === "projectCode") { + orderBy = sql`${projects.code} ${direction}` + } else if (sortField.id in codeGroups) { + const col = codeGroups[sortField.id as keyof typeof codeGroups] + orderBy = sql`${col} ${direction}` + } } } - // 데이터 조회 + // 데이터 조회 (프로젝트 정보 포함) const data = await db .select({ id: codeGroups.id, @@ -97,32 +105,36 @@ export async function getComboBoxCodeGroups(input: { isActive: codeGroups.isActive, createdAt: codeGroups.createdAt, updatedAt: codeGroups.updatedAt, + projectId: codeGroups.projectId, + projectCode: projects.code, + projectName: projects.name, }) .from(codeGroups) + .leftJoin(projects, eq(codeGroups.projectId, projects.id)) .where(whereConditions) .orderBy(orderBy) .limit(perPage) .offset(offset) - // 총 개수 조회 - const [{ count: total }] = await db - .select({ count: count() }) + // 총 개수 조회 (프로젝트 정보 포함) + const totalCountResult = await db + .select({ count: sql<number>`count(*)` }) .from(codeGroups) + .leftJoin(projects, eq(codeGroups.projectId, projects.id)) .where(whereConditions) - const pageCount = Math.ceil(total / perPage) + const totalCount = totalCountResult[0]?.count || 0 return { - success: true, data, - pageCount, + totalCount, + pageCount: Math.ceil(totalCount / perPage), } } catch (error) { console.error("Error fetching combo box code groups:", error) return { - success: false, - error: "Failed to fetch combo box code groups", data: [], + totalCount: 0, pageCount: 0, } } @@ -150,7 +162,8 @@ export async function getComboBoxOptions(codeGroupId: number, input?: { whereConditions = sql`${whereConditions} AND ( ${comboBoxSettings.code} ILIKE ${searchTerm} OR ${comboBoxSettings.description} ILIKE ${searchTerm} OR - ${comboBoxSettings.remark} ILIKE ${searchTerm} + ${comboBoxSettings.remark} ILIKE ${searchTerm} OR + ${projects.code} ILIKE ${searchTerm} )` } @@ -159,14 +172,20 @@ export async function getComboBoxOptions(codeGroupId: number, input?: { if (sort && sort.length > 0) { const sortField = sort[0] // 안전성 체크: 필드가 실제 테이블에 존재하는지 확인 - if (sortField && sortField.id && typeof sortField.id === "string" && sortField.id in comboBoxSettings) { + if (sortField && sortField.id && typeof sortField.id === "string") { const direction = sortField.desc ? sql`DESC` : sql`ASC` - const col = comboBoxSettings[sortField.id as keyof typeof comboBoxSettings] - orderBy = sql`${col} ${direction}` + + // 프로젝트 코드 정렬 처리 + if (sortField.id === "projectCode") { + orderBy = sql`${projects.code} ${direction}` + } else if (sortField.id in comboBoxSettings) { + const col = comboBoxSettings[sortField.id as keyof typeof comboBoxSettings] + orderBy = sql`${col} ${direction}` + } } } - // 데이터 조회 + // 데이터 조회 (프로젝트 정보 포함) const data = await db .select({ id: comboBoxSettings.id, @@ -176,32 +195,36 @@ export async function getComboBoxOptions(codeGroupId: number, input?: { remark: comboBoxSettings.remark, createdAt: comboBoxSettings.createdAt, updatedAt: comboBoxSettings.updatedAt, + projectId: comboBoxSettings.projectId, + projectCode: projects.code, + projectName: projects.name, }) .from(comboBoxSettings) + .leftJoin(projects, eq(comboBoxSettings.projectId, projects.id)) .where(whereConditions) .orderBy(orderBy) .limit(perPage) .offset(offset) - // 총 개수 조회 - const [{ count: total }] = await db - .select({ count: count() }) + // 총 개수 조회 (프로젝트 정보 포함) + const totalCountResult = await db + .select({ count: sql<number>`count(*)` }) .from(comboBoxSettings) + .leftJoin(projects, eq(comboBoxSettings.projectId, projects.id)) .where(whereConditions) - const pageCount = Math.ceil(total / perPage) + const totalCount = totalCountResult[0]?.count || 0 return { - success: true, data, - pageCount, + totalCount, + pageCount: Math.ceil(totalCount / perPage), } } catch (error) { console.error("Error fetching combo box options:", error) return { - success: false, - error: "Failed to fetch combo box options", data: [], + totalCount: 0, pageCount: 0, } } 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 049e2c1a..a0535b43 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 @@ -27,7 +27,7 @@ import { } from "@/components/ui/form" import { Input } from "@/components/ui/input" -import { createComboBoxOption } from "../service" +import { createComboBoxOption } from "@/lib/docu-list-rule/combo-box-settings/service" const createOptionSchema = z.object({ code: z.string().min(1, "코드는 필수입니다."), 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 b62b258e..22806ae8 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 @@ -14,7 +14,7 @@ import { getColumns } from "@/lib/docu-list-rule/combo-box-settings/table/combo- 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" +import { codeGroups } from "@/db/schema/docu-list-rule" type ComboBoxOption = { id: number codeGroupId: number 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 4ac539d0..e4504d8c 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 @@ -25,7 +25,7 @@ import { } from "@/components/ui/form" import { Input } from "@/components/ui/input" -import { updateComboBoxOption } from "../service" +import { updateComboBoxOption } from "@/lib/docu-list-rule/combo-box-settings/service" const updateOptionSchema = z.object({ code: z.string().min(1, "코드는 필수입니다."), 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 0e46c0ed..17754331 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 @@ -28,6 +28,9 @@ interface ComboBoxOption { isActive?: boolean createdAt: Date updatedAt: Date + projectId: number + projectCode: string | null + projectName: string | null } interface GetColumnsProps { @@ -111,6 +114,19 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<ComboBo // ---------------------------------------------------------------- const dataColumns: ColumnDef<ComboBoxOption>[] = [ { + accessorKey: "projectCode", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트 코드" /> + ), + meta: { + excelHeader: "프로젝트 코드", + type: "text", + }, + cell: ({ row }) => row.getValue("projectCode") ?? "", + minSize: 120 + }, + { accessorKey: "code", enableResizing: true, header: ({ column }) => ( @@ -124,6 +140,19 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<ComboBo minSize: 80 }, { + accessorKey: "description", + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="description" /> + ), + meta: { + excelHeader: "description", + type: "text", + }, + cell: ({ row }) => row.getValue("description") ?? "", + minSize: 80 + }, + { accessorKey: "remark", enableResizing: true, header: ({ column }) => ( diff --git a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-table-toolbar.tsx b/lib/docu-list-rule/combo-box-settings/table/combo-box-options-table-toolbar.tsx index 7318efb8..3bb3c95f 100644 --- a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-table-toolbar.tsx +++ b/lib/docu-list-rule/combo-box-settings/table/combo-box-options-table-toolbar.tsx @@ -3,8 +3,8 @@ import * as React from "react" import { type Table } from "@tanstack/react-table" -import { ComboBoxOptionsAddDialog } from "./combo-box-options-add-dialog" -import { DeleteComboBoxOptionsDialog } from "./delete-combo-box-options-dialog" +import { ComboBoxOptionsAddDialog } from "@/lib/docu-list-rule/combo-box-settings/table/combo-box-options-add-dialog" +import { DeleteComboBoxOptionsDialog } from "@/lib/docu-list-rule/combo-box-settings/table/delete-combo-box-options-dialog" interface ComboBoxOption { id: number 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 efce54b4..d41fe5ec 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 @@ -92,6 +92,19 @@ export function getColumns({ onDetail }: GetColumnsProps): ColumnDef<typeof code // ---------------------------------------------------------------- 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/combo-box-settings/table/combo-box-settings-table.tsx b/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table.tsx index 42ce1a19..8e469149 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 @@ -5,8 +5,8 @@ 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 { getColumns } from "./combo-box-settings-table-columns" -import { ComboBoxOptionsDetailSheet } from "./combo-box-options-detail-sheet" +import { getColumns } from "@/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table-columns" +import { ComboBoxOptionsDetailSheet } from "@/lib/docu-list-rule/combo-box-settings/table/combo-box-options-detail-sheet" import { codeGroups } from "@/db/schema/docu-list-rule" interface ComboBoxSettingsTableProps { @@ -61,11 +61,30 @@ export function ComboBoxSettingsTable({ promises }: ComboBoxSettingsTableProps) }, ], }, - getRowId: (originalRow) => String(originalRow.groupId), + getRowId: (originalRow) => String(originalRow.id), shallow: false, clearOnDefault: true, }) + // 컴포넌트 마운트 후 그룹핑 설정 + React.useEffect(() => { + if (rawData[0]?.data && table.getState().grouping.length === 0) { + table.setGrouping(["projectCode"]) + } + }, [table, rawData]) + + // 정렬 시 펼쳐진 상태 유지 + React.useEffect(() => { + const currentExpanded = table.getState().expanded + if (Object.keys(currentExpanded).length > 0) { + // 약간의 지연 후 현재 펼쳐진 상태를 다시 설정 + const timer = setTimeout(() => { + table.setExpanded(currentExpanded) + }, 100) + return () => clearTimeout(timer) + } + }, [table.getState().sorting, table]) + return ( <> <DataTable table={table}> diff --git a/lib/docu-list-rule/combo-box-settings/table/delete-combo-box-options-dialog.tsx b/lib/docu-list-rule/combo-box-settings/table/delete-combo-box-options-dialog.tsx index e3d8bd23..4d9e2455 100644 --- a/lib/docu-list-rule/combo-box-settings/table/delete-combo-box-options-dialog.tsx +++ b/lib/docu-list-rule/combo-box-settings/table/delete-combo-box-options-dialog.tsx @@ -27,7 +27,7 @@ import { DrawerTrigger, } from "@/components/ui/drawer" -import { deleteComboBoxOption } from "../service" +import { deleteComboBoxOption } from "@/lib/docu-list-rule/combo-box-settings/service" interface ComboBoxOption { id: number diff --git a/lib/docu-list-rule/document-class/service.ts b/lib/docu-list-rule/document-class/service.ts index a1bb14a7..91a4e053 100644 --- a/lib/docu-list-rule/document-class/service.ts +++ b/lib/docu-list-rule/document-class/service.ts @@ -3,7 +3,8 @@ import { revalidatePath } from "next/cache" import db from "@/db/db" import { documentClasses, documentClassOptions, codeGroups } from "@/db/schema/docu-list-rule" -import { eq, desc, asc, sql, and } from "drizzle-orm" +import { projects } from "@/db/schema/projects" +import { eq, desc, sql, and } from "drizzle-orm" // Document Class 목록 조회 (A Class, B Class 등) export async function getDocumentClassCodeGroups(input: { @@ -31,7 +32,8 @@ export async function getDocumentClassCodeGroups(input: { whereConditions = sql`${whereConditions} AND ( ${documentClasses.code} ILIKE ${searchTerm} OR ${documentClasses.value} ILIKE ${searchTerm} OR - ${documentClasses.description} ILIKE ${searchTerm} + ${documentClasses.description} ILIKE ${searchTerm} OR + ${projects.code} ILIKE ${searchTerm} )` } @@ -48,6 +50,8 @@ export async function getDocumentClassCodeGroups(input: { return sql`${documentClasses.value} ILIKE ${`%${value}%`}` case "description": return sql`${documentClasses.description} ILIKE ${`%${value}%`}` + case "projectCode": + return sql`${projects.code} ILIKE ${`%${value}%`}` case "isActive": return sql`${documentClasses.isActive} = ${value === "true"}` case "createdAt": @@ -73,14 +77,20 @@ export async function getDocumentClassCodeGroups(input: { if (sort && sort.length > 0) { const sortField = sort[0] // 안전성 체크: 필드가 실제 테이블에 존재하는지 확인 - if (sortField && sortField.id && typeof sortField.id === "string" && sortField.id in documentClasses) { + if (sortField && sortField.id && typeof sortField.id === "string") { const direction = sortField.desc ? sql`DESC` : sql`ASC` - const col = documentClasses[sortField.id as keyof typeof documentClasses] - orderBy = sql`${col} ${direction}` + + // 프로젝트 코드 정렬 처리 + if (sortField.id === "projectCode") { + orderBy = sql`${projects.code} ${direction}` + } else if (sortField.id in documentClasses) { + const col = documentClasses[sortField.id as keyof typeof documentClasses] + orderBy = sql`${col} ${direction}` + } } } - // 데이터 조회 + // 데이터 조회 (프로젝트 정보 포함) const data = await db .select({ id: documentClasses.id, @@ -90,32 +100,36 @@ export async function getDocumentClassCodeGroups(input: { isActive: documentClasses.isActive, createdAt: documentClasses.createdAt, updatedAt: documentClasses.updatedAt, + projectId: documentClasses.projectId, + projectCode: projects.code, + projectName: projects.name, }) .from(documentClasses) + .leftJoin(projects, eq(documentClasses.projectId, projects.id)) .where(whereConditions) .orderBy(orderBy) .limit(perPage) .offset(offset) - // 총 개수 조회 - const [{ count: total }] = await db - .select({ count: sql`count(*)` }) + // 총 개수 조회 (프로젝트 정보 포함) + const totalCountResult = await db + .select({ count: sql<number>`count(*)` }) .from(documentClasses) + .leftJoin(projects, eq(documentClasses.projectId, projects.id)) .where(whereConditions) - const pageCount = Math.ceil(Number(total) / perPage) + const totalCount = totalCountResult[0]?.count || 0 return { - success: true, data, - pageCount, + totalCount, + pageCount: Math.ceil(totalCount / perPage), } } catch (error) { console.error("Error fetching document classes:", error) return { - success: false, - error: "Failed to fetch document classes", data: [], + totalCount: 0, pageCount: 0, } } @@ -123,14 +137,15 @@ export async function getDocumentClassCodeGroups(input: { // Document Class 생성 export async function createDocumentClassCodeGroup(input: { + projectId: number // projectCode를 projectId로 변경 value: string description?: string }) { try { // Value 자동 변환: "A", "AB", "A Class", "A CLASS" 등을 "A Class", "AB Class" 형태로 변환 - const formatValue = (value: string): string => { + const formatValue = (input: string): string => { // 공백 제거 및 대소문자 정규화 - const cleaned = value.trim().toLowerCase() + const cleaned = input.trim().toLowerCase() // "class"가 포함되어 있으면 제거 const withoutClass = cleaned.replace(/\s*class\s*/g, '') @@ -139,7 +154,7 @@ export async function createDocumentClassCodeGroup(input: { const letters = withoutClass.replace(/[^a-z0-9]/g, '') if (letters.length === 0) { - return value.trim() // 변환할 수 없으면 원본 반환 + return input.trim() // 변환할 수 없으면 원본 반환 } // 첫 글자를 대문자로 변환하고 "Class" 추가 @@ -148,10 +163,11 @@ export async function createDocumentClassCodeGroup(input: { const formattedValue = formatValue(input.value) - // 자동으로 code 생성 (예: "DOC_CLASS_001", "DOC_CLASS_002" 등) + // 해당 프로젝트의 자동으로 code 생성 (예: "DOC_CLASS_001", "DOC_CLASS_002" 등) const existingClasses = await db .select({ code: documentClasses.code }) .from(documentClasses) + .where(eq(documentClasses.projectId, input.projectId)) // projectId로 변경 .orderBy(desc(documentClasses.code)) let newCode = "DOC_CLASS_001" @@ -163,11 +179,14 @@ export async function createDocumentClassCodeGroup(input: { } } - // Code Group이 존재하는지 확인 + // 해당 프로젝트의 Code Group이 존재하는지 확인 const existingCodeGroup = await db .select({ id: codeGroups.id }) .from(codeGroups) - .where(eq(codeGroups.groupId, 'DOC_CLASS')) + .where(and( + eq(codeGroups.projectId, input.projectId), // projectId로 변경 + eq(codeGroups.groupId, 'DOC_CLASS') + )) .limit(1) let codeGroupId: number | null = null @@ -177,6 +196,7 @@ export async function createDocumentClassCodeGroup(input: { const [newCodeGroup] = await db .insert(codeGroups) .values({ + projectId: input.projectId, // projectId로 변경 groupId: 'DOC_CLASS', description: 'Document Class', codeFormat: 'DOC_CLASS_###', @@ -194,6 +214,7 @@ export async function createDocumentClassCodeGroup(input: { const [newDocumentClass] = await db .insert(documentClasses) .values({ + projectId: input.projectId, // projectId로 변경 code: newCode, value: formattedValue, description: input.description || "", diff --git a/lib/docu-list-rule/document-class/table/delete-document-class-dialog.tsx b/lib/docu-list-rule/document-class/table/delete-document-class-dialog.tsx index e81e4df6..08e73a36 100644 --- a/lib/docu-list-rule/document-class/table/delete-document-class-dialog.tsx +++ b/lib/docu-list-rule/document-class/table/delete-document-class-dialog.tsx @@ -29,7 +29,7 @@ import { } from "@/components/ui/drawer" import { deleteDocumentClassCodeGroup, getDocumentClassOptionsCount } from "@/lib/docu-list-rule/document-class/service" -import { documentClasses } from "@/db/schema" +import { documentClasses } from "@/db/schema/docu-list-rule" interface DeleteDocumentClassDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> { diff --git a/lib/docu-list-rule/document-class/table/delete-document-class-option-dialog.tsx b/lib/docu-list-rule/document-class/table/delete-document-class-option-dialog.tsx index 34ce239f..4ac4eae0 100644 --- a/lib/docu-list-rule/document-class/table/delete-document-class-option-dialog.tsx +++ b/lib/docu-list-rule/document-class/table/delete-document-class-option-dialog.tsx @@ -28,7 +28,7 @@ import { } from "@/components/ui/drawer" import { deleteDocumentClassOption } from "@/lib/docu-list-rule/document-class/service" -import { documentClassOptions } from "@/db/schema" +import { documentClassOptions } from "@/db/schema/docu-list-rule" interface DeleteDocumentClassOptionDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> { 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 ef9c50a8..dfd1d7f2 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,11 +26,19 @@ 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" const createDocumentClassSchema = z.object({ + projectId: z.string().min(1, "프로젝트는 필수입니다."), value: z.string().min(1, "Value는 필수입니다."), description: z.string().optional(), }) @@ -46,20 +54,49 @@ export function DocumentClassAddDialog({ }: DocumentClassAddDialogProps) { const [open, setOpen] = React.useState(false) const [isPending, startTransition] = React.useTransition() + const [projects, setProjects] = React.useState<Array<{ id: number; code: string; name: 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: [] + }) + if (result.data) { + setProjects(result.data) + } + } 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({ + const result = await createDocumentClassCodeGroup({ + projectId: parseInt(input.projectId), value: input.value, description: input.description, }) @@ -94,16 +131,41 @@ export function DocumentClassAddDialog({ </DialogTrigger> <DialogContent className="sm:max-w-[425px]"> <DialogHeader> - <DialogTitle>Document Class 추가</DialogTitle> - <DialogDescription> - 새로운 Document Class를 추가합니다. - <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span> - </DialogDescription> + <DialogTitle>Document Class 추가</DialogTitle> + <DialogDescription> + 새로운 Document Class를 추가합니다. + <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span> + </DialogDescription> </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} name="value" render={({ field }) => ( <FormItem> diff --git a/lib/docu-list-rule/document-class/table/document-class-edit-sheet.tsx b/lib/docu-list-rule/document-class/table/document-class-edit-sheet.tsx index 5ad23b22..32c1976d 100644 --- a/lib/docu-list-rule/document-class/table/document-class-edit-sheet.tsx +++ b/lib/docu-list-rule/document-class/table/document-class-edit-sheet.tsx @@ -28,7 +28,7 @@ import { import { Input } from "@/components/ui/input" import { updateDocumentClassCodeGroup } from "@/lib/docu-list-rule/document-class/service" -import { documentClasses } from "@/db/schema" +import { documentClasses } from "@/db/schema/docu-list-rule" const updateDocumentClassSchema = z.object({ value: z.string().min(1, "Value는 필수입니다."), diff --git a/lib/docu-list-rule/document-class/table/document-class-option-edit-sheet.tsx b/lib/docu-list-rule/document-class/table/document-class-option-edit-sheet.tsx index bc2318c6..8444285e 100644 --- a/lib/docu-list-rule/document-class/table/document-class-option-edit-sheet.tsx +++ b/lib/docu-list-rule/document-class/table/document-class-option-edit-sheet.tsx @@ -26,7 +26,7 @@ import { import { Input } from "@/components/ui/input" import { updateDocumentClassOption } from "@/lib/docu-list-rule/document-class/service" -import { documentClassOptions } from "@/db/schema" +import { documentClassOptions } from "@/db/schema/docu-list-rule" const updateOptionSchema = z.object({ optionCode: z.string().min(1, "코드는 필수입니다."), 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 8c391def..ad8494c7 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,6 +107,20 @@ 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-toolbar.tsx b/lib/docu-list-rule/document-class/table/document-class-table-toolbar.tsx index 9b43f43d..a9ab660a 100644 --- a/lib/docu-list-rule/document-class/table/document-class-table-toolbar.tsx +++ b/lib/docu-list-rule/document-class/table/document-class-table-toolbar.tsx @@ -3,9 +3,9 @@ import * as React from "react" import { type Table } from "@tanstack/react-table" -import { DeleteDocumentClassDialog } from "./delete-document-class-dialog" -import { DocumentClassAddDialog } from "./document-class-add-dialog" -import { documentClasses } from "@/db/schema" +import { DeleteDocumentClassDialog } from "@/lib/docu-list-rule/document-class/table/delete-document-class-dialog" +import { DocumentClassAddDialog } from "@/lib/docu-list-rule/document-class/table/document-class-add-dialog" +import { documentClasses } from "@/db/schema/docu-list-rule" interface DocumentClassTableToolbarActionsProps { table: Table<typeof documentClasses.$inferSelect> 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 c66a1395..c9156ff7 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 @@ -46,9 +46,9 @@ export function DocumentClassTable({ promises }: DocumentClassTableProps) { ] const { table } = useDataTable({ - data: rawData[0].data as typeof documentClasses.$inferSelect[], + data: rawData[0]?.data as typeof documentClasses.$inferSelect[] || [], columns, - pageCount: rawData[0].pageCount, + pageCount: rawData[0]?.pageCount || 0, enablePinning: true, enableAdvancedFilter: true, initialState: { @@ -59,6 +59,26 @@ export function DocumentClassTable({ promises }: DocumentClassTableProps) { clearOnDefault: true, }) + + // 컴포넌트 마운트 후 그룹핑 설정 + React.useEffect(() => { + if (rawData[0]?.data && table.getState().grouping.length === 0) { + table.setGrouping(["projectCode"]) + } + }, [table, rawData]) + + // 정렬 시 펼쳐진 상태 유지 + React.useEffect(() => { + const currentExpanded = table.getState().expanded + if (Object.keys(currentExpanded).length > 0) { + // 약간의 지연 후 현재 펼쳐진 상태를 다시 설정 + const timer = setTimeout(() => { + table.setExpanded(currentExpanded) + }, 100) + return () => clearTimeout(timer) + } + }, [table.getState().sorting, table]) + return ( <> <DataTable table={table}> diff --git a/lib/docu-list-rule/number-type-configs/service.ts b/lib/docu-list-rule/number-type-configs/service.ts index 0b0bb905..ef25aecb 100644 --- a/lib/docu-list-rule/number-type-configs/service.ts +++ b/lib/docu-list-rule/number-type-configs/service.ts @@ -4,7 +4,8 @@ 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 { asc, eq, sql, count, and } from "drizzle-orm" +import { projects } from "@/db/schema/projects" +import { asc, eq, sql, and } from "drizzle-orm" import { GetNumberTypeConfigsSchema } from "./validation" // 특정 Number Type의 Configs 조회 @@ -18,8 +19,8 @@ export async function getNumberTypeConfigs(input: GetNumberTypeConfigsSchema) { // numberTypeId 유효성 검사 if (!numberTypeId || numberTypeId <= 0) { return { - success: true, data: [], + totalCount: 0, pageCount: 0, } } @@ -33,7 +34,8 @@ export async function getNumberTypeConfigs(input: GetNumberTypeConfigsSchema) { whereConditions = sql`${whereConditions} AND ( ${codeGroups.description} ILIKE ${searchTerm} OR ${documentNumberTypeConfigs.description} ILIKE ${searchTerm} OR - ${documentNumberTypeConfigs.remark} ILIKE ${searchTerm} + ${documentNumberTypeConfigs.remark} ILIKE ${searchTerm} OR + ${projects.code} ILIKE ${searchTerm} )` } @@ -102,12 +104,11 @@ export async function getNumberTypeConfigs(input: GetNumberTypeConfigsSchema) { default: col = documentNumberTypeConfigs.sdq } - orderBy = sql`${col} ${direction}` } } - // 데이터 조회 + // 데이터 조회 (프로젝트 정보 포함) const data = await db .select({ id: documentNumberTypeConfigs.id, @@ -121,34 +122,38 @@ export async function getNumberTypeConfigs(input: GetNumberTypeConfigsSchema) { updatedAt: documentNumberTypeConfigs.updatedAt, codeGroupName: codeGroups.description, codeGroupControlType: codeGroups.controlType, + projectId: documentNumberTypeConfigs.projectId, + projectCode: projects.code, + projectName: projects.name, }) .from(documentNumberTypeConfigs) .leftJoin(codeGroups, eq(documentNumberTypeConfigs.codeGroupId, codeGroups.id)) + .leftJoin(projects, eq(documentNumberTypeConfigs.projectId, projects.id)) .where(whereConditions) .orderBy(orderBy) .limit(perPage) .offset(offset) - // 총 개수 조회 - const [{ count: total }] = await db - .select({ count: count() }) + // 총 개수 조회 (프로젝트 정보 포함) + const totalCountResult = await db + .select({ count: sql<number>`count(*)` }) .from(documentNumberTypeConfigs) .leftJoin(codeGroups, eq(documentNumberTypeConfigs.codeGroupId, codeGroups.id)) + .leftJoin(projects, eq(documentNumberTypeConfigs.projectId, projects.id)) .where(whereConditions) - const pageCount = Math.ceil(total / perPage) + const totalCount = totalCountResult[0]?.count || 0 return { - success: true, data, - pageCount, + totalCount, + pageCount: Math.ceil(totalCount / perPage), } } catch (error) { console.error("Error fetching number type configs:", error) return { - success: false, - error: "Failed to fetch number type configs", data: [], + totalCount: 0, pageCount: 0, } } @@ -161,6 +166,7 @@ export async function createNumberTypeConfig(input: { sdq: number description?: string remark?: string + projectId: number }) { try { const [result] = await db @@ -171,6 +177,7 @@ export async function createNumberTypeConfig(input: { sdq: input.sdq, description: input.description, remark: input.remark, + projectId: input.projectId, }) .returning({ id: documentNumberTypeConfigs.id }) @@ -284,10 +291,17 @@ export async function deleteNumberTypeConfig(id: number) { } // 활성화된 Code Groups 조회 (Config 생성/수정 시 사용) -export async function getActiveCodeGroups() { +export async function getActiveCodeGroups(projectId?: number) { unstable_noStore() try { + let whereConditions = eq(codeGroups.isActive, true) + + // 프로젝트별 필터링 추가 + if (projectId) { + whereConditions = and(whereConditions, eq(codeGroups.projectId, projectId)) + } + const codeGroupsData = await db .select({ id: codeGroups.id, @@ -295,9 +309,10 @@ export async function getActiveCodeGroups() { description: codeGroups.description, controlType: codeGroups.controlType, isActive: codeGroups.isActive, + projectId: codeGroups.projectId, }) .from(codeGroups) - .where(eq(codeGroups.isActive, true)) + .where(whereConditions) .orderBy(asc(codeGroups.description)) return { diff --git a/lib/docu-list-rule/number-type-configs/table/delete-number-type-configs-dialog.tsx b/lib/docu-list-rule/number-type-configs/table/delete-number-type-configs-dialog.tsx index 96c7e7c7..cc3c8d93 100644 --- a/lib/docu-list-rule/number-type-configs/table/delete-number-type-configs-dialog.tsx +++ b/lib/docu-list-rule/number-type-configs/table/delete-number-type-configs-dialog.tsx @@ -27,8 +27,8 @@ import { DrawerTrigger, } from "@/components/ui/drawer" -import { deleteNumberTypeConfig } from "../service" -import { NumberTypeConfig } from "../../types" +import { deleteNumberTypeConfig } from "@/lib/docu-list-rule/number-type-configs/service" +import { NumberTypeConfig } from "@/lib/docu-list-rule/types" interface DeleteNumberTypeConfigsDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> { diff --git a/lib/docu-list-rule/number-type-configs/table/number-type-configs-edit-dialog.tsx b/lib/docu-list-rule/number-type-configs/table/number-type-configs-edit-dialog.tsx index 69d84a3f..cd2d6fc8 100644 --- a/lib/docu-list-rule/number-type-configs/table/number-type-configs-edit-dialog.tsx +++ b/lib/docu-list-rule/number-type-configs/table/number-type-configs-edit-dialog.tsx @@ -24,8 +24,8 @@ import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" -import { updateNumberTypeConfig, getActiveCodeGroups } from "../service" -import { NumberTypeConfig } from "../../types" +import { updateNumberTypeConfig, getActiveCodeGroups } from "@/lib/docu-list-rule/number-type-configs/service" +import { NumberTypeConfig } from "@/lib/docu-list-rule/types" interface NumberTypeConfigsEditDialogProps { open: boolean @@ -33,6 +33,7 @@ interface NumberTypeConfigsEditDialogProps { data: NumberTypeConfig | null onSuccess?: () => void existingConfigs?: NumberTypeConfig[] // 기존 configs 목록 추가 + selectedProjectId?: number | null } export function NumberTypeConfigsEditDialog({ @@ -41,6 +42,7 @@ export function NumberTypeConfigsEditDialog({ data, onSuccess, existingConfigs = [], // 기본값 추가 + selectedProjectId, }: NumberTypeConfigsEditDialogProps) { const [isLoading, setIsLoading] = React.useState(false) const [codeGroups, setCodeGroups] = React.useState<{ id: number; description: string }[]>([]) @@ -67,7 +69,7 @@ export function NumberTypeConfigsEditDialog({ React.useEffect(() => { (async () => { try { - const result = await getActiveCodeGroups() + const result = await getActiveCodeGroups(selectedProjectId || undefined) if (result.success && result.data) { setCodeGroups(result.data) } @@ -75,7 +77,7 @@ export function NumberTypeConfigsEditDialog({ console.error("Error loading code groups:", error) } })() - }, []) + }, [selectedProjectId]) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() 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 b03000e0..24255acf 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 @@ -15,7 +15,7 @@ import { import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" import { Checkbox } from "@/components/ui/checkbox" import type { DataTableRowAction } from "@/types/table" -import { NumberTypeConfig } from "../../types" +import { NumberTypeConfig } from "@/lib/docu-list-rule/types" interface GetColumnsProps { setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<NumberTypeConfig> | null>> @@ -50,6 +50,19 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<NumberT 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, header: ({ column }) => ( 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 3e4dd262..a6ba3e50 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 @@ -8,27 +8,35 @@ import { arrayMove } from '@dnd-kit/sortable' import { DragEndEvent } from '@dnd-kit/core' import { toast } from "sonner" -import { getNumberTypeConfigs, updateNumberTypeConfig } from "../service" -import { getColumns } from "./number-type-configs-table-columns" -import { DeleteNumberTypeConfigsDialog } from "./delete-number-type-configs-dialog" -import { NumberTypeConfigsEditDialog } from "./number-type-configs-edit-dialog" -import { NumberTypeSelector } from "./number-type-selector" -import { DragDropTable } from "./drag-drop-table" -import { NumberTypeConfigsToolbarActions } from "./number-type-configs-toolbar-actions" +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" +import { NumberTypeSelector } from "@/lib/docu-list-rule/number-type-configs/table/number-type-selector" +import { DragDropTable } from "@/lib/docu-list-rule/number-type-configs/table/drag-drop-table" +import { NumberTypeConfigsToolbarActions } from "@/lib/docu-list-rule/number-type-configs/table/number-type-configs-toolbar-actions" import { documentNumberTypes } from "@/db/schema/docu-list-rule" -import { NumberTypeConfig } from "../../types" -import { GetNumberTypeConfigsSchema } from "../validation" +import { NumberTypeConfig } from "@/lib/docu-list-rule/types" +import { GetNumberTypeConfigsSchema } from "@/lib/docu-list-rule/number-type-configs/validation" + +// Number Type with project info type +type NumberTypeWithProject = typeof documentNumberTypes.$inferSelect & { + projectCode: string | null + projectName: string | null +} interface NumberTypeConfigsTableProps { - promises?: Promise<[{ data: typeof documentNumberTypes.$inferSelect[]; pageCount: number }]> + promises?: Promise<[{ data: NumberTypeWithProject[]; pageCount: number }]> searchParams: GetNumberTypeConfigsSchema } export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeConfigsTableProps) { const rawData = React.use(promises!) + const [selectedProjectId, setSelectedProjectId] = React.useState<number | null>(null) 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) + const isInitialLoad = React.useRef(true) // configs 데이터 로드 함수 const fetchConfigs = React.useCallback(async (numberTypeId: number, params?: Partial<GetNumberTypeConfigsSchema>) => { @@ -38,7 +46,7 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon ...params, numberTypeId, }) - if (result.success && result.data) { + if (result.data) { setConfigsData({ data: result.data, pageCount: result.pageCount }) } else { setConfigsData({ data: [], pageCount: 0 }) @@ -55,12 +63,22 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon try { const result = rawData[0] - if (result.data && result.data.length > 0) { - const firstNumberTypeId = result.data[0].id - setSelectedNumberType(firstNumberTypeId) + if (result.data && result.data.length > 0 && isInitialLoad.current) { + // 초기 로드 시 첫 번째 프로젝트의 첫 번째 Number Type을 선택 + const firstProjectId = result.data[0].projectId + setSelectedProjectId(firstProjectId) + + 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(firstNumberTypeId) + // 초기 로드 완료 표시 + isInitialLoad.current = false } } catch (error) { @@ -70,13 +88,38 @@ 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]) // Number Type 변경 핸들러 (서버 사이드 처리 지원) const handleNumberTypeChange = React.useCallback((numberTypeId: number) => { setSelectedNumberType(numberTypeId) - // searchParams를 업데이트하여 서버 사이드 필터링 적용 - fetchConfigs(numberTypeId, { page: 1 }) // 페이지 리셋 + + // numberTypeId가 0이면 선택 해제된 것이므로 configs 데이터 초기화 + if (numberTypeId === 0) { + setConfigsData({ data: [], pageCount: 0 }) + } else { + // searchParams를 업데이트하여 서버 사이드 필터링 적용 + fetchConfigs(numberTypeId, { page: 1 }) // 페이지 리셋 + } }, [fetchConfigs]) // 드래그 종료 핸들러 @@ -205,6 +248,8 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon numberTypes={rawData[0]?.data || []} selectedNumberType={selectedNumberType} onNumberTypeChange={handleNumberTypeChange} + selectedProjectId={selectedProjectId} + onProjectChange={handleProjectChange} isLoading={!rawData[0]?.data} /> @@ -223,6 +268,7 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon table={table} onSuccess={refreshData} selectedNumberType={selectedNumberType} + selectedProjectId={selectedProjectId} configsData={configsData.data} /> </DataTableAdvancedToolbar> @@ -246,6 +292,7 @@ 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-configs-toolbar-actions.tsx b/lib/docu-list-rule/number-type-configs/table/number-type-configs-toolbar-actions.tsx index b30ee268..572d05cd 100644 --- a/lib/docu-list-rule/number-type-configs/table/number-type-configs-toolbar-actions.tsx +++ b/lib/docu-list-rule/number-type-configs/table/number-type-configs-toolbar-actions.tsx @@ -24,15 +24,16 @@ import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" import { toast } from "sonner" -import { createNumberTypeConfig, getActiveCodeGroups } from "../service" -import { DeleteNumberTypeConfigsDialog } from "./delete-number-type-configs-dialog" -import { NumberTypeConfig } from "../../types" +import { createNumberTypeConfig, getActiveCodeGroups } from "@/lib/docu-list-rule/number-type-configs/service" +import { DeleteNumberTypeConfigsDialog } from "@/lib/docu-list-rule/number-type-configs/table/delete-number-type-configs-dialog" +import { NumberTypeConfig } from "@/lib/docu-list-rule/types" interface NumberTypeConfigsToolbarActionsProps { // eslint-disable-next-line @typescript-eslint/no-explicit-any table: any onSuccess?: () => void selectedNumberType: number | null + selectedProjectId: number | null configsData: NumberTypeConfig[] } @@ -40,6 +41,7 @@ export function NumberTypeConfigsToolbarActions({ table, onSuccess, selectedNumberType, + selectedProjectId, configsData }: NumberTypeConfigsToolbarActionsProps) { const [isAddDialogOpen, setIsAddDialogOpen] = React.useState(false) @@ -50,7 +52,7 @@ export function NumberTypeConfigsToolbarActions({ const loadCodeGroups = React.useCallback(async () => { try { - const result = await getActiveCodeGroups() + const result = await getActiveCodeGroups(selectedProjectId || undefined) if (result.success && result.data) { // 이미 추가된 Code Group들을 제외하고 필터링 @@ -63,7 +65,7 @@ export function NumberTypeConfigsToolbarActions({ } catch (error) { console.error("Error details:", error) } - }, [configsData]) + }, [configsData, selectedProjectId]) React.useEffect(() => { loadCodeGroups() 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 c311730e..fe8d0895 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,11 +2,26 @@ 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 & { + projectCode: string | null + projectName: string | null +} interface NumberTypeSelectorProps { - numberTypes: typeof documentNumberTypes.$inferSelect[] + numberTypes: NumberTypeWithProject[] selectedNumberType: number | null onNumberTypeChange: (numberTypeId: number) => void + selectedProjectId: number | null + onProjectChange: (projectId: number | null) => void isLoading?: boolean } @@ -14,14 +29,64 @@ 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"> - <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을 불러오는 중... + <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을 불러오는 중... + </div> </div> </div> ) @@ -29,32 +94,73 @@ export function NumberTypeSelector({ if (!numberTypes || numberTypes.length === 0) { return ( - <div className="mb-6"> - <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이 없습니다. + <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이 없습니다. + </div> </div> </div> ) } return ( - <div className="mb-6"> - <label className="text-sm font-medium mb-2 block">Number Type 선택</label> - <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 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} ({project.numberTypes.length}개) + </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> </div> ) diff --git a/lib/docu-list-rule/number-types/service.ts b/lib/docu-list-rule/number-types/service.ts index 12c3bf73..a7c32274 100644 --- a/lib/docu-list-rule/number-types/service.ts +++ b/lib/docu-list-rule/number-types/service.ts @@ -3,7 +3,8 @@ import { revalidatePath } from "next/cache" import db from "@/db/db" import { documentNumberTypes, documentNumberTypeConfigs } from "@/db/schema/docu-list-rule" -import { eq, sql } from "drizzle-orm" +import { projects } from "@/db/schema/projects" +import { eq, sql, and } from "drizzle-orm" import { unstable_noStore } from "next/cache" // Number Types 목록 조회 @@ -33,7 +34,8 @@ export async function getNumberTypes(input: { const searchTerm = `%${search}%` whereConditions = sql`${whereConditions} AND ( ${documentNumberTypes.name} ILIKE ${searchTerm} OR - ${documentNumberTypes.description} ILIKE ${searchTerm} + ${documentNumberTypes.description} ILIKE ${searchTerm} OR + ${projects.code} ILIKE ${searchTerm} )` } @@ -73,14 +75,20 @@ export async function getNumberTypes(input: { if (sort && sort.length > 0) { const sortField = sort[0] // 안전성 체크: 필드가 실제 테이블에 존재하는지 확인 - if (sortField && sortField.id && typeof sortField.id === "string" && sortField.id in documentNumberTypes) { + if (sortField && sortField.id && typeof sortField.id === "string") { const direction = sortField.desc ? sql`DESC` : sql`ASC` - const col = documentNumberTypes[sortField.id as keyof typeof documentNumberTypes] - orderBy = sql`${col} ${direction}` + + // 프로젝트 코드 정렬 처리 + if (sortField.id === "projectCode") { + orderBy = sql`${projects.code} ${direction}` + } else if (sortField.id in documentNumberTypes) { + const col = documentNumberTypes[sortField.id as keyof typeof documentNumberTypes] + orderBy = sql`${col} ${direction}` + } } } - // 데이터 조회 + // 데이터 조회 (프로젝트 정보 포함) const data = await db .select({ id: documentNumberTypes.id, @@ -89,32 +97,36 @@ export async function getNumberTypes(input: { isActive: documentNumberTypes.isActive, createdAt: documentNumberTypes.createdAt, updatedAt: documentNumberTypes.updatedAt, + projectId: documentNumberTypes.projectId, + projectCode: projects.code, + projectName: projects.name, }) .from(documentNumberTypes) + .leftJoin(projects, eq(documentNumberTypes.projectId, projects.id)) .where(whereConditions) .orderBy(orderBy) .limit(perPage) .offset(offset) - // 총 개수 조회 - const [{ count: total }] = await db - .select({ count: sql`count(*)` }) + // 총 개수 조회 (프로젝트 정보 포함) + const totalCountResult = await db + .select({ count: sql<number>`count(*)` }) .from(documentNumberTypes) + .leftJoin(projects, eq(documentNumberTypes.projectId, projects.id)) .where(whereConditions) - const pageCount = Math.ceil(Number(total) / perPage) + const totalCount = totalCountResult[0]?.count || 0 return { - success: true, data, - pageCount, + totalCount, + pageCount: Math.ceil(totalCount / perPage), } } catch (error) { console.error("Error fetching number types:", error) return { - success: false, - error: "Failed to fetch number types", data: [], + totalCount: 0, pageCount: 0, } } @@ -122,28 +134,33 @@ export async function getNumberTypes(input: { // Number Type 생성 export async function createNumberType(input: { + projectId: number name: string description?: string isActive?: boolean }) { try { - // 중복 이름 체크 + // 해당 프로젝트에서 중복 이름 체크 const existing = await db .select({ id: documentNumberTypes.id }) .from(documentNumberTypes) - .where(eq(documentNumberTypes.name, input.name)) + .where(and( + eq(documentNumberTypes.projectId, input.projectId), + eq(documentNumberTypes.name, input.name) + )) .limit(1) if (existing.length > 0) { return { success: false, - error: "Number Type with this name already exists" + error: "Number Type with this name already exists in this project" } } const [newNumberType] = await db .insert(documentNumberTypes) .values({ + projectId: input.projectId, name: input.name, description: input.description, isActive: input.isActive ?? true, 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 964a6bd0..c48eb217 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 @@ -1,7 +1,6 @@ "use client" import * as React from "react" -import { useState } from "react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" @@ -15,24 +14,68 @@ 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" interface NumberTypeAddDialogProps { onSuccess: () => void } export function NumberTypeAddDialog({ onSuccess }: NumberTypeAddDialogProps) { - const [open, setOpen] = useState(false) - const [loading, setLoading] = useState(false) - const [formData, setFormData] = useState({ + const [open, setOpen] = React.useState(false) + const [loading, setLoading] = React.useState(false) + const [projects, setProjects] = React.useState<Array<{ id: number; code: string; name: 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) { + setProjects(result.data) + } + } 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 @@ -41,13 +84,14 @@ export function NumberTypeAddDialog({ onSuccess }: NumberTypeAddDialogProps) { setLoading(true) try { const result = await createNumberType({ + projectId: parseInt(formData.projectId), name: formData.name.trim(), description: formData.description.trim() || undefined, }) if (result.success) { toast.success("Number Type이 생성되었습니다.") - setFormData({ name: "", description: "" }) + setFormData({ projectId: "", name: "", description: "" }) setOpen(false) onSuccess() } else { @@ -62,7 +106,7 @@ export function NumberTypeAddDialog({ onSuccess }: NumberTypeAddDialogProps) { } const handleCancel = () => { - setFormData({ name: "", description: "" }) + setFormData({ projectId: "", name: "", description: "" }) setOpen(false) } @@ -79,10 +123,26 @@ export function NumberTypeAddDialog({ onSuccess }: NumberTypeAddDialogProps) { <DialogTitle>Add Number Type</DialogTitle> <DialogDescription> 새로운 Number Type을 추가합니다. + <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span> </DialogDescription> </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" @@ -113,7 +173,7 @@ export function NumberTypeAddDialog({ onSuccess }: NumberTypeAddDialogProps) { </Button> <Button type="submit" - disabled={loading} + disabled={loading || !formData.projectId || !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 93361b93..e4bfc345 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 @@ -101,6 +101,19 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof // ---------------------------------------------------------------- const dataColumns: ColumnDef<typeof documentNumberTypes.$inferSelect>[] = [ { + 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 18aa1a30..66f789d1 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 @@ -55,6 +55,25 @@ 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(() => { + const currentExpanded = table.getState().expanded + if (Object.keys(currentExpanded).length > 0) { + // 약간의 지연 후 현재 펼쳐진 상태를 다시 설정 + const timer = setTimeout(() => { + table.setExpanded(currentExpanded) + }, 100) + return () => clearTimeout(timer) + } + }, [table.getState().sorting, table]) + return ( <> <DataTable table={table}> diff --git a/lib/docu-list-rule/types.ts b/lib/docu-list-rule/types.ts index cb80316a..ef3f90d3 100644 --- a/lib/docu-list-rule/types.ts +++ b/lib/docu-list-rule/types.ts @@ -24,6 +24,9 @@ export interface CodeGroup { isActive: boolean createdAt: Date updatedAt: Date + projectId: number + projectCode: string | null + projectName: string | null } export interface DocumentClass { @@ -35,6 +38,9 @@ export interface DocumentClass { isActive: boolean createdAt: Date updatedAt: Date + projectId: number + projectCode: string | null + projectName: string | null } export interface NumberType { @@ -44,4 +50,7 @@ export interface NumberType { isActive: boolean createdAt: Date updatedAt: Date + projectId: number + projectCode: string | null + projectName: string | null }
\ No newline at end of file diff --git a/lib/docu-list-rule/utils.ts b/lib/docu-list-rule/utils.ts index ddeb5e6d..bc9260af 100644 --- a/lib/docu-list-rule/utils.ts +++ b/lib/docu-list-rule/utils.ts @@ -1,7 +1,7 @@ // docu-list-rule 모듈 공통 유틸리티 함수들 /** - * Code Group ID에서 다음 번호를 생성하는 함수 + * Code Group ID에서 다음 번호를 생성하는 함수 (프로젝트별) * DOC_CLASS는 제외하고 계산 */ export function generateNextCodeGroupId(lastGroupId: string): string { @@ -15,7 +15,7 @@ export function generateNextCodeGroupId(lastGroupId: string): string { } /** - * Document Class Code에서 다음 번호를 생성하는 함수 + * Document Class Code에서 다음 번호를 생성하는 함수 (프로젝트별) */ export function generateNextDocumentClassCode(lastCode: string): string { if (!lastCode.startsWith('DOC_CLASS_')) { @@ -28,7 +28,7 @@ export function generateNextDocumentClassCode(lastCode: string): string { } /** - * Number Type Config에서 다음 SDQ를 생성하는 함수 + * Number Type Config에서 다음 SDQ를 생성하는 함수 (프로젝트별) */ export function generateNextSdq(configs: Array<{ sdq: number }>): number { if (configs.length === 0) { |
