summaryrefslogtreecommitdiff
path: root/lib/docu-list-rule
diff options
context:
space:
mode:
author0-Zz-ang <s1998319@gmail.com>2025-08-07 18:02:54 +0900
committer0-Zz-ang <s1998319@gmail.com>2025-08-07 18:02:54 +0900
commit67bb1ad7d7e001e19c8d1dd9153a5f663e2afa03 (patch)
tree9aab25663d6c180fd3a315840a3035b491ac0b7d /lib/docu-list-rule
parente270e477f362dd68249bb4a013c66eab293bba82 (diff)
(박서영)docu-list-rule Project_code적용
Diffstat (limited to 'lib/docu-list-rule')
-rw-r--r--lib/docu-list-rule/code-groups/service.ts48
-rw-r--r--lib/docu-list-rule/code-groups/table/code-groups-add-dialog.tsx61
-rw-r--r--lib/docu-list-rule/code-groups/table/code-groups-edit-sheet.tsx4
-rw-r--r--lib/docu-list-rule/code-groups/table/code-groups-table-columns.tsx13
-rw-r--r--lib/docu-list-rule/code-groups/table/code-groups-table-toolbar.tsx6
-rw-r--r--lib/docu-list-rule/code-groups/table/code-groups-table.tsx34
-rw-r--r--lib/docu-list-rule/code-groups/table/delete-code-groups-dialog.tsx4
-rw-r--r--lib/docu-list-rule/combo-box-settings/service.ts75
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-options-add-dialog.tsx2
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-options-detail-sheet.tsx2
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-options-edit-sheet.tsx2
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-options-table-columns.tsx29
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-options-table-toolbar.tsx4
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table-columns.tsx13
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table.tsx25
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/delete-combo-box-options-dialog.tsx2
-rw-r--r--lib/docu-list-rule/document-class/service.ts61
-rw-r--r--lib/docu-list-rule/document-class/table/delete-document-class-dialog.tsx2
-rw-r--r--lib/docu-list-rule/document-class/table/delete-document-class-option-dialog.tsx2
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx76
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-edit-sheet.tsx2
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-option-edit-sheet.tsx2
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-table-columns.tsx14
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-table-toolbar.tsx6
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-table.tsx24
-rw-r--r--lib/docu-list-rule/number-type-configs/service.ts45
-rw-r--r--lib/docu-list-rule/number-type-configs/table/delete-number-type-configs-dialog.tsx4
-rw-r--r--lib/docu-list-rule/number-type-configs/table/number-type-configs-edit-dialog.tsx10
-rw-r--r--lib/docu-list-rule/number-type-configs/table/number-type-configs-table-columns.tsx15
-rw-r--r--lib/docu-list-rule/number-type-configs/table/number-type-configs-table.tsx85
-rw-r--r--lib/docu-list-rule/number-type-configs/table/number-type-configs-toolbar-actions.tsx12
-rw-r--r--lib/docu-list-rule/number-type-configs/table/number-type-selector.tsx156
-rw-r--r--lib/docu-list-rule/number-types/service.ts51
-rw-r--r--lib/docu-list-rule/number-types/table/number-type-add-dialog.tsx74
-rw-r--r--lib/docu-list-rule/number-types/table/number-types-table-columns.tsx13
-rw-r--r--lib/docu-list-rule/number-types/table/number-types-table.tsx19
-rw-r--r--lib/docu-list-rule/types.ts9
-rw-r--r--lib/docu-list-rule/utils.ts6
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) {