summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/docu-list-rule/code-groups/service.ts89
-rw-r--r--lib/docu-list-rule/code-groups/table/code-groups-table-columns.tsx1
-rw-r--r--lib/docu-list-rule/code-groups/table/code-groups-table.tsx9
-rw-r--r--lib/docu-list-rule/combo-box-settings/service.ts94
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-options-detail-sheet.tsx8
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table.tsx7
-rw-r--r--lib/docu-list-rule/document-class/service.ts59
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-table.tsx7
-rw-r--r--lib/docu-list-rule/number-type-configs/repository.ts269
-rw-r--r--lib/docu-list-rule/number-type-configs/service.ts238
-rw-r--r--lib/docu-list-rule/number-type-configs/table/drag-drop-table.tsx193
-rw-r--r--lib/docu-list-rule/number-type-configs/table/number-type-configs-edit-dialog.tsx3
-rw-r--r--lib/docu-list-rule/number-type-configs/table/number-type-configs-table.tsx712
-rw-r--r--lib/docu-list-rule/number-type-configs/table/number-type-configs-toolbar-actions.tsx251
-rw-r--r--lib/docu-list-rule/number-type-configs/table/number-type-selector.tsx61
-rw-r--r--lib/docu-list-rule/number-type-configs/validation.ts26
-rw-r--r--lib/docu-list-rule/number-types/service.ts60
-rw-r--r--lib/docu-list-rule/number-types/table/number-types-table.tsx6
18 files changed, 1227 insertions, 866 deletions
diff --git a/lib/docu-list-rule/code-groups/service.ts b/lib/docu-list-rule/code-groups/service.ts
index 34ec5610..2c30cedb 100644
--- a/lib/docu-list-rule/code-groups/service.ts
+++ b/lib/docu-list-rule/code-groups/service.ts
@@ -6,28 +6,18 @@ import { codeGroups, comboBoxSettings, documentClasses } from "@/db/schema/docu-
import { eq, sql, count } from "drizzle-orm"
import { unstable_noStore } from "next/cache"
-// Code Groups 목록 조회
-export async function getCodeGroups(input: {
- page: number
- perPage: number
- search?: string
- sort?: Array<{ id: string; desc: boolean }>
- filters?: Array<{ id: string; value: string }>
- joinOperator?: "and" | "or"
- flags?: string[]
- groupId?: string
- description?: string
- controlType?: string
- isActive?: string
-} | any) {
+// Code Groups 목록 조회
+export async function getCodeGroups(input: any) {
unstable_noStore()
try {
- const { page, perPage, sort, search } = input
+ const { page, perPage, search, filters, joinOperator } = input
const offset = (page - 1) * perPage
// 검색 조건 (Document Class 제외)
let whereConditions = sql`${codeGroups.groupId} != 'DOC_CLASS'`
+
+ // 검색어 필터링
if (search) {
const searchTerm = `%${search}%`
whereConditions = sql`${codeGroups.groupId} != 'DOC_CLASS' AND (
@@ -38,33 +28,50 @@ export async function getCodeGroups(input: {
)`
}
- // 정렬
+ // 고급 필터링 (단순화)
+ if (filters && filters.length > 0) {
+ const filterConditions = filters.map(filter => {
+ const { id, value } = filter
+ if (!value || Array.isArray(value)) return null
+
+ switch (id) {
+ case "groupId":
+ return sql`${codeGroups.groupId} ILIKE ${`%${value}%`}`
+ case "description":
+ return sql`${codeGroups.description} ILIKE ${`%${value}%`}`
+ case "codeFormat":
+ return sql`${codeGroups.codeFormat} ILIKE ${`%${value}%`}`
+ case "controlType":
+ return sql`${codeGroups.controlType} = ${value}`
+ case "isActive":
+ return sql`${codeGroups.isActive} = ${value === "true"}`
+ case "createdAt":
+ return sql`${codeGroups.createdAt}::text ILIKE ${`%${value}%`}`
+ default:
+ return null
+ }
+ }).filter(Boolean)
+
+ if (filterConditions.length > 0) {
+ const operator = joinOperator === "or" ? sql` OR ` : sql` AND `
+ const combinedFilters = filterConditions.reduce((acc, condition, index) => {
+ if (index === 0) return condition
+ return sql`${acc}${operator}${condition}`
+ })
+
+ whereConditions = sql`${whereConditions} AND (${combinedFilters})`
+ }
+ }
+
+ // 정렬 (안전한 필드 체크 적용)
let orderBy = sql`${codeGroups.createdAt} DESC`
- if (sort && sort.length > 0) {
- const sortField = sort[0]
- const direction = sortField.desc ? sql`DESC` : sql`ASC`
-
- switch (sortField.id) {
- case "groupId":
- orderBy = sql`${codeGroups.groupId} ${direction}`
- break
- case "description":
- orderBy = sql`${codeGroups.description} ${direction}`
- break
- case "codeFormat":
- orderBy = sql`${codeGroups.codeFormat} ${direction}`
- break
- case "controlType":
- orderBy = sql`${codeGroups.controlType} ${direction}`
- break
- case "isActive":
- orderBy = sql`${codeGroups.isActive} ${direction}`
- break
- case "createdAt":
- orderBy = sql`${codeGroups.createdAt} ${direction}`
- break
- default:
- orderBy = sql`${codeGroups.createdAt} DESC`
+ if (input.sort && input.sort.length > 0) {
+ const sortField = input.sort[0]
+ // 안전성 체크: 필드가 실제 테이블에 존재하는지 확인
+ if (sortField && sortField.id && typeof sortField.id === "string" && sortField.id in codeGroups) {
+ const direction = sortField.desc ? sql`DESC` : sql`ASC`
+ const col = codeGroups[sortField.id as keyof typeof codeGroups]
+ orderBy = sql`${col} ${direction}`
}
}
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 cb6cdf8b..c15dd676 100644
--- a/lib/docu-list-rule/code-groups/table/code-groups-table-columns.tsx
+++ b/lib/docu-list-rule/code-groups/table/code-groups-table-columns.tsx
@@ -1,3 +1,4 @@
+
"use client"
import * as React from "react"
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 6d8bb907..c10d3445 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
@@ -1,5 +1,6 @@
"use client";
import * as React from "react";
+import { useRouter } from "next/navigation";
import { useDataTable } from "@/hooks/use-data-table";
import { DataTable } from "@/components/data-table/data-table";
import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar";
@@ -8,7 +9,6 @@ import type {
DataTableFilterField,
DataTableRowAction,
} from "@/types/table"
-import { getCodeGroups } from "../service";
import { getColumns } from "./code-groups-table-columns";
import { DeleteCodeGroupsDialog } from "./delete-code-groups-dialog";
import { CodeGroupsEditSheet } from "./code-groups-edit-sheet";
@@ -20,14 +20,15 @@ interface CodeGroupsTableProps {
}
export function CodeGroupsTable({ promises }: CodeGroupsTableProps) {
+ const router = useRouter();
const [rowAction, setRowAction] = React.useState<DataTableRowAction<typeof codeGroups.$inferSelect> | null>(null);
const [{ data, pageCount }] = promises ? React.use(promises) : [{ data: [], pageCount: 0 }];
const refreshData = React.useCallback(async () => {
- // 페이지 새로고침으로 처리
- window.location.reload();
- }, []);
+ // 전체 페이지 새로고침 대신 router.refresh() 사용 (성능 개선)
+ router.refresh();
+ }, [router]);
// 컬럼 설정 - 외부 파일에서 가져옴
const columns = React.useMemo(
diff --git a/lib/docu-list-rule/combo-box-settings/service.ts b/lib/docu-list-rule/combo-box-settings/service.ts
index b603ee71..70046828 100644
--- a/lib/docu-list-rule/combo-box-settings/service.ts
+++ b/lib/docu-list-rule/combo-box-settings/service.ts
@@ -22,7 +22,7 @@ export async function getComboBoxCodeGroups(input: {
unstable_noStore()
try {
- const { page, perPage, sort, search } = input
+ const { page, perPage, sort, search, filters, joinOperator } = input
const offset = (page - 1) * perPage
// Control Type이 combobox인 조건
@@ -38,33 +38,50 @@ export async function getComboBoxCodeGroups(input: {
)`
}
- // 정렬
+ // 고급 필터링
+ if (filters && filters.length > 0) {
+ const filterConditions = filters.map(filter => {
+ const { id, value } = filter
+ if (!value) return null
+
+ switch (id) {
+ case "groupId":
+ return sql`${codeGroups.groupId} ILIKE ${`%${value}%`}`
+ case "description":
+ return sql`${codeGroups.description} ILIKE ${`%${value}%`}`
+ case "codeFormat":
+ return sql`${codeGroups.codeFormat} ILIKE ${`%${value}%`}`
+ case "controlType":
+ return sql`${codeGroups.controlType} = ${value}`
+ case "isActive":
+ return sql`${codeGroups.isActive} = ${value === "true"}`
+ case "createdAt":
+ return sql`${codeGroups.createdAt}::text ILIKE ${`%${value}%`}`
+ default:
+ return null
+ }
+ }).filter(Boolean)
+
+ if (filterConditions.length > 0) {
+ const operator = joinOperator === "or" ? sql` OR ` : sql` AND `
+ const combinedFilters = filterConditions.reduce((acc, condition, index) => {
+ if (index === 0) return condition
+ return sql`${acc}${operator}${condition}`
+ })
+
+ whereConditions = sql`${whereConditions} AND (${combinedFilters})`
+ }
+ }
+
+ // 정렬 (안전한 필드 체크 적용)
let orderBy = sql`${codeGroups.createdAt} DESC`
if (sort && sort.length > 0) {
const sortField = sort[0]
- const direction = sortField.desc ? sql`DESC` : sql`ASC`
-
- switch (sortField.id) {
- case "groupId":
- orderBy = sql`${codeGroups.groupId} ${direction}`
- break
- case "description":
- orderBy = sql`${codeGroups.description} ${direction}`
- break
- case "codeFormat":
- orderBy = sql`${codeGroups.codeFormat} ${direction}`
- break
- case "controlType":
- orderBy = sql`${codeGroups.controlType} ${direction}`
- break
- case "isActive":
- orderBy = sql`${codeGroups.isActive} ${direction}`
- break
- case "createdAt":
- orderBy = sql`${codeGroups.createdAt} ${direction}`
- break
- default:
- orderBy = sql`${codeGroups.createdAt} DESC`
+ // 안전성 체크: 필드가 실제 테이블에 존재하는지 확인
+ if (sortField && sortField.id && typeof sortField.id === "string" && sortField.id in codeGroups) {
+ const direction = sortField.desc ? sql`DESC` : sql`ASC`
+ const col = codeGroups[sortField.id as keyof typeof codeGroups]
+ orderBy = sql`${col} ${direction}`
}
}
@@ -137,30 +154,15 @@ export async function getComboBoxOptions(codeGroupId: number, input?: {
)`
}
- // 정렬
+ // 정렬 (안전한 필드 체크 적용)
let orderBy = sql`${comboBoxSettings.createdAt} DESC`
if (sort && sort.length > 0) {
const sortField = sort[0]
- const direction = sortField.desc ? sql`DESC` : sql`ASC`
-
- switch (sortField.id) {
- case "code":
- orderBy = sql`${comboBoxSettings.code} ${direction}`
- break
- case "description":
- orderBy = sql`${comboBoxSettings.description} ${direction}`
- break
- case "remark":
- orderBy = sql`${comboBoxSettings.remark} ${direction}`
- break
- case "createdAt":
- orderBy = sql`${comboBoxSettings.createdAt} ${direction}`
- break
- case "updatedAt":
- orderBy = sql`${comboBoxSettings.updatedAt} ${direction}`
- break
- default:
- orderBy = sql`${comboBoxSettings.createdAt} DESC`
+ // 안전성 체크: 필드가 실제 테이블에 존재하는지 확인
+ if (sortField && sortField.id && typeof sortField.id === "string" && sortField.id in comboBoxSettings) {
+ const direction = sortField.desc ? sql`DESC` : sql`ASC`
+ const col = comboBoxSettings[sortField.id as keyof typeof comboBoxSettings]
+ orderBy = sql`${col} ${direction}`
}
}
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 1c145c55..0b2a76a4 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
@@ -88,13 +88,12 @@ export function ComboBoxOptionsDetailSheet({
const result = await getComboBoxOptions(codeGroup.id, {
page: 1,
perPage: 10,
- search: "",
- sort: [{ id: "createdAt", desc: true }],
- filters: [],
+ search: table.getState().globalFilter || "",
+ sort: table.getState().sorting,
+ filters: table.getState().columnFilters,
joinOperator: "and",
})
if (result.success && result.data) {
- // isActive 필드가 없는 경우 기본값 true로 설정
const optionsWithIsActive = result.data.map(option => ({
...option,
isActive: (option as any).isActive ?? true
@@ -126,6 +125,7 @@ export function ComboBoxOptionsDetailSheet({
enableAdvancedFilter: true,
manualSorting: true,
manualFiltering: true,
+ manualPagination: true, // 수동 페이징 활성화
initialState: {
sorting: [{ id: "createdAt", desc: true }],
columnPinning: { right: ["actions"] },
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 356b2706..f6216363 100644
--- a/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table.tsx
+++ b/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table.tsx
@@ -1,6 +1,7 @@
"use client"
import * as React from "react"
+import { useRouter } from "next/navigation"
import { useDataTable } from "@/hooks/use-data-table"
import { DataTable } from "@/components/data-table/data-table"
import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
@@ -16,13 +17,15 @@ interface ComboBoxSettingsTableProps {
}
export function ComboBoxSettingsTable({ promises }: ComboBoxSettingsTableProps) {
+ const router = useRouter()
const rawData = React.use(promises!)
const [isDetailSheetOpen, setIsDetailSheetOpen] = React.useState(false)
const [selectedCodeGroup, setSelectedCodeGroup] = React.useState<typeof codeGroups.$inferSelect | null>(null)
const refreshData = React.useCallback(() => {
- window.location.reload()
- }, [])
+ // 전체 페이지 새로고침 대신 router.refresh() 사용 (성능 개선)
+ router.refresh()
+ }, [router])
// Detail 버튼 클릭 핸들러
const handleDetail = (codeGroup: typeof codeGroups.$inferSelect) => {
diff --git a/lib/docu-list-rule/document-class/service.ts b/lib/docu-list-rule/document-class/service.ts
index 04dfa50e..99d85ea5 100644
--- a/lib/docu-list-rule/document-class/service.ts
+++ b/lib/docu-list-rule/document-class/service.ts
@@ -19,7 +19,7 @@ export async function getDocumentClassCodeGroups(input: {
isActive?: string
}) {
try {
- const { page, perPage, sort, search } = input
+ const { page, perPage, sort, search, filters, joinOperator } = input
const offset = (page - 1) * perPage
// 기본 조건
@@ -35,27 +35,48 @@ export async function getDocumentClassCodeGroups(input: {
)`
}
- // 정렬
+ // 고급 필터링
+ if (filters && filters.length > 0) {
+ const filterConditions = filters.map(filter => {
+ const { id, value } = filter
+ if (!value) return null
+
+ switch (id) {
+ case "code":
+ return sql`${documentClasses.code} ILIKE ${`%${value}%`}`
+ case "value":
+ return sql`${documentClasses.value} ILIKE ${`%${value}%`}`
+ case "description":
+ return sql`${documentClasses.description} ILIKE ${`%${value}%`}`
+ case "isActive":
+ return sql`${documentClasses.isActive} = ${value === "true"}`
+ case "createdAt":
+ return sql`${documentClasses.createdAt}::text ILIKE ${`%${value}%`}`
+ default:
+ return null
+ }
+ }).filter(Boolean)
+
+ if (filterConditions.length > 0) {
+ const operator = joinOperator === "or" ? sql` OR ` : sql` AND `
+ const combinedFilters = filterConditions.reduce((acc, condition, index) => {
+ if (index === 0) return condition
+ return sql`${acc}${operator}${condition}`
+ })
+
+ whereConditions = sql`${whereConditions} AND (${combinedFilters})`
+ }
+ }
+
+ // 정렬 (안전한 필드 체크 적용)
let orderBy = sql`${documentClasses.createdAt} DESC`
if (sort && sort.length > 0) {
const sortField = sort[0]
- const direction = sortField.desc ? sql`DESC` : sql`ASC`
-
- switch (sortField.id) {
- case "code":
- orderBy = sql`${documentClasses.code} ${direction}`
- break
- case "value":
- orderBy = sql`${documentClasses.value} ${direction}`
- break
- case "description":
- orderBy = sql`${documentClasses.description} ${direction}`
- break
- case "createdAt":
- orderBy = sql`${documentClasses.createdAt} ${direction}`
- break
- default:
- orderBy = sql`${documentClasses.createdAt} DESC`
+ // 안전성 체크: 필드가 실제 테이블에 존재하는지 확인
+ if (sortField && sortField.id && typeof sortField.id === "string" && sortField.id in documentClasses) {
+ const direction = sortField.desc ? sql`DESC` : sql`ASC`
+ const col = documentClasses[sortField.id as keyof typeof documentClasses]
+ orderBy = sql`${col} ${direction}`
}
}
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 bbe79800..e3daac8a 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
@@ -1,6 +1,7 @@
"use client"
import * as React from "react"
+import { useRouter } from "next/navigation"
import { useDataTable } from "@/hooks/use-data-table"
import { DataTable } from "@/components/data-table/data-table"
import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
@@ -18,13 +19,15 @@ interface DocumentClassTableProps {
}
export function DocumentClassTable({ promises }: DocumentClassTableProps) {
+ const router = useRouter()
const rawData = React.use(promises!)
const [rowAction, setRowAction] = React.useState<DataTableRowAction<typeof documentClasses.$inferSelect> | null>(null)
const [selectedDocumentClass, setSelectedDocumentClass] = React.useState<typeof documentClasses.$inferSelect | null>(null)
const refreshData = React.useCallback(() => {
- window.location.reload()
- }, [])
+ // 전체 페이지 새로고침 대신 router.refresh() 사용 (성능 개선)
+ router.refresh()
+ }, [router])
const columns = React.useMemo(() => getColumns({ setRowAction }), [setRowAction])
diff --git a/lib/docu-list-rule/number-type-configs/repository.ts b/lib/docu-list-rule/number-type-configs/repository.ts
new file mode 100644
index 00000000..56be1bcf
--- /dev/null
+++ b/lib/docu-list-rule/number-type-configs/repository.ts
@@ -0,0 +1,269 @@
+import db from "@/db/db"
+import { documentNumberTypeConfigs } from "@/db/schema/docu-list-rule"
+import { codeGroups, documentClasses } from "@/db/schema/docu-list-rule"
+import { eq, asc, sql, count, ilike, or, and } from "drizzle-orm"
+import { PgTransaction } from "drizzle-orm/pg-core"
+
+// Number Type Configs 조회 (고급 필터링 지원)
+export async function selectNumberTypeConfigs(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ tx: PgTransaction<any, any, any> | typeof db,
+ params: {
+ numberTypeId: number
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ where?: any
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ orderBy?: any[]
+ offset?: number
+ limit?: number
+ search?: string
+ }
+) {
+ const { numberTypeId, where, orderBy, offset = 0, limit = 10, search } = params
+
+ let query = tx
+ .select({
+ id: documentNumberTypeConfigs.id,
+ documentNumberTypeId: documentNumberTypeConfigs.documentNumberTypeId,
+ codeGroupId: documentNumberTypeConfigs.codeGroupId,
+ documentClassId: documentNumberTypeConfigs.documentClassId,
+ sdq: documentNumberTypeConfigs.sdq,
+ description: documentNumberTypeConfigs.description,
+ remark: documentNumberTypeConfigs.remark,
+ isActive: documentNumberTypeConfigs.isActive,
+ createdAt: documentNumberTypeConfigs.createdAt,
+ updatedAt: documentNumberTypeConfigs.updatedAt,
+ // Code Group 정보도 함께 가져오기
+ codeGroupName: codeGroups.description,
+ codeGroupControlType: codeGroups.controlType,
+ // Document Class 정보도 함께 가져오기
+ documentClassName: documentClasses.value,
+ documentClassDescription: documentClasses.description,
+ })
+ .from(documentNumberTypeConfigs)
+ .leftJoin(codeGroups, eq(documentNumberTypeConfigs.codeGroupId, codeGroups.id))
+ .leftJoin(documentClasses, eq(documentNumberTypeConfigs.documentClassId, documentClasses.id))
+
+ // 기본 조건: 특정 Number Type
+ let whereCondition = eq(documentNumberTypeConfigs.documentNumberTypeId, numberTypeId)
+
+ // 고급 필터 조건 추가
+ if (where) {
+ whereCondition = and(whereCondition, where) || whereCondition
+ }
+
+ // 검색 조건 추가
+ if (search) {
+ const searchTerm = `%${search}%`
+ const searchCondition = or(
+ ilike(codeGroups.description, searchTerm),
+ ilike(documentNumberTypeConfigs.description, searchTerm),
+ ilike(documentNumberTypeConfigs.remark, searchTerm),
+ ilike(documentClasses.value, searchTerm)
+ )
+ if (searchCondition) {
+ whereCondition = and(whereCondition, searchCondition) || whereCondition
+ }
+ }
+
+ query = query.where(whereCondition)
+
+ // 정렬 적용
+ if (orderBy && orderBy.length > 0) {
+ query = query.orderBy(...orderBy)
+ } else {
+ query = query.orderBy(asc(documentNumberTypeConfigs.sdq))
+ }
+
+ return query.offset(offset).limit(limit)
+}
+
+// Number Type Configs 개수 조회
+export async function countNumberTypeConfigs(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ tx: PgTransaction<any, any, any> | typeof db,
+ params: {
+ numberTypeId: number
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ where?: any
+ search?: string
+ }
+) {
+ const { numberTypeId, where, search } = params
+
+ let query = tx
+ .select({ count: count() })
+ .from(documentNumberTypeConfigs)
+ .leftJoin(codeGroups, eq(documentNumberTypeConfigs.codeGroupId, codeGroups.id))
+ .leftJoin(documentClasses, eq(documentNumberTypeConfigs.documentClassId, documentClasses.id))
+
+ // 기본 조건: 특정 Number Type
+ let whereCondition = eq(documentNumberTypeConfigs.documentNumberTypeId, numberTypeId)
+
+ // 고급 필터 조건 추가
+ if (where) {
+ whereCondition = and(whereCondition, where) || whereCondition
+ }
+
+ // 검색 조건 추가
+ if (search) {
+ const searchTerm = `%${search}%`
+ const searchCondition = or(
+ ilike(codeGroups.description, searchTerm),
+ ilike(documentNumberTypeConfigs.description, searchTerm),
+ ilike(documentNumberTypeConfigs.remark, searchTerm),
+ ilike(documentClasses.value, searchTerm)
+ )
+ if (searchCondition) {
+ whereCondition = and(whereCondition, searchCondition) || whereCondition
+ }
+ }
+
+ const result = await query.where(whereCondition)
+ return result[0]?.count ?? 0
+}
+
+// Number Type Config 생성
+export async function insertNumberTypeConfig(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ tx: PgTransaction<any, any, any> | typeof db,
+ input: {
+ documentNumberTypeId: number
+ codeGroupId: number | null
+ documentClassId: number | null
+ sdq: number
+ description?: string
+ remark?: string
+ }
+) {
+ return tx
+ .insert(documentNumberTypeConfigs)
+ .values({
+ documentNumberTypeId: input.documentNumberTypeId,
+ codeGroupId: input.codeGroupId,
+ documentClassId: input.documentClassId,
+ sdq: input.sdq,
+ description: input.description,
+ remark: input.remark,
+ })
+ .returning({ id: documentNumberTypeConfigs.id })
+}
+
+// Number Type Config 수정
+export async function updateNumberTypeConfigById(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ tx: PgTransaction<any, any, any> | typeof db,
+ id: number,
+ input: {
+ codeGroupId: number | null
+ documentClassId: number | null
+ sdq: number
+ description?: string
+ remark?: string
+ }
+) {
+ return tx
+ .update(documentNumberTypeConfigs)
+ .set({
+ codeGroupId: input.codeGroupId,
+ documentClassId: input.documentClassId,
+ sdq: input.sdq,
+ description: input.description,
+ remark: input.remark,
+ updatedAt: new Date(),
+ })
+ .where(eq(documentNumberTypeConfigs.id, id))
+ .returning({ id: documentNumberTypeConfigs.id })
+}
+
+// Number Type Config 삭제
+export async function deleteNumberTypeConfigById(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ tx: PgTransaction<any, any, any> | typeof db,
+ id: number
+) {
+ return tx
+ .delete(documentNumberTypeConfigs)
+ .where(eq(documentNumberTypeConfigs.id, id))
+}
+
+// 삭제할 Config 정보 조회
+export async function selectConfigToDelete(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ tx: PgTransaction<any, any, any> | typeof db,
+ id: number
+) {
+ return tx
+ .select({
+ documentNumberTypeId: documentNumberTypeConfigs.documentNumberTypeId,
+ sdq: documentNumberTypeConfigs.sdq,
+ })
+ .from(documentNumberTypeConfigs)
+ .where(eq(documentNumberTypeConfigs.id, id))
+}
+
+// 순서 재정렬 (삭제 후)
+export async function reorderConfigsAfterDelete(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ tx: PgTransaction<any, any, any> | typeof db,
+ documentNumberTypeId: number,
+ deletedSdq: number
+) {
+ return tx
+ .update(documentNumberTypeConfigs)
+ .set({
+ sdq: sql`${documentNumberTypeConfigs.sdq} - 1`,
+ updatedAt: new Date(),
+ })
+ .where(
+ sql`${documentNumberTypeConfigs.documentNumberTypeId} = ${documentNumberTypeId} AND ${documentNumberTypeConfigs.sdq} > ${deletedSdq}`
+ )
+}
+
+// 활성화된 Code Groups 조회
+export async function selectActiveCodeGroups(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ tx: PgTransaction<any, any, any> | typeof db
+) {
+ return tx
+ .select({
+ id: codeGroups.id,
+ groupId: codeGroups.groupId,
+ description: codeGroups.description,
+ controlType: codeGroups.controlType,
+ isActive: codeGroups.isActive,
+ })
+ .from(codeGroups)
+ .where(eq(codeGroups.isActive, true))
+ .orderBy(asc(codeGroups.description))
+}
+
+// Document Class Code Group ID 조회
+export async function selectDocumentClassCodeGroupId(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ tx: PgTransaction<any, any, any> | typeof db
+) {
+ return tx
+ .select({ id: codeGroups.id })
+ .from(codeGroups)
+ .where(eq(codeGroups.groupId, 'DOC_CLASS'))
+ .limit(1)
+}
+
+// 활성화된 Document Classes 조회
+export async function selectActiveDocumentClasses(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ tx: PgTransaction<any, any, any> | typeof db
+) {
+ return tx
+ .select({
+ id: documentClasses.id,
+ code: documentClasses.code,
+ value: documentClasses.value,
+ description: documentClasses.description,
+ isActive: documentClasses.isActive,
+ })
+ .from(documentClasses)
+ .where(eq(documentClasses.isActive, true))
+ .orderBy(asc(documentClasses.value))
+} \ No newline at end of file
diff --git a/lib/docu-list-rule/number-type-configs/service.ts b/lib/docu-list-rule/number-type-configs/service.ts
index 3e2cfc8e..1ba9b8f0 100644
--- a/lib/docu-list-rule/number-type-configs/service.ts
+++ b/lib/docu-list-rule/number-type-configs/service.ts
@@ -2,60 +2,96 @@
import { revalidatePath } from "next/cache"
import db from "@/db/db"
-import { documentNumberTypes, documentNumberTypeConfigs } from "@/db/schema/docu-list-rule"
-import { codeGroups, documentClasses } from "@/db/schema/docu-list-rule"
-import { eq, asc, sql } from "drizzle-orm"
import { unstable_noStore } from "next/cache"
+import { unstable_cache } from "@/lib/unstable-cache"
+import { filterColumns } from "@/lib/filter-columns"
+import { documentNumberTypeConfigs, codeGroups, documentClasses } from "@/db/schema/docu-list-rule"
+import { asc, desc, eq, sql } from "drizzle-orm"
+import {
+ selectNumberTypeConfigs,
+ countNumberTypeConfigs,
+ insertNumberTypeConfig,
+ updateNumberTypeConfigById,
+ deleteNumberTypeConfigById,
+ selectConfigToDelete,
+ reorderConfigsAfterDelete,
+ selectActiveCodeGroups,
+ selectDocumentClassCodeGroupId,
+ selectActiveDocumentClasses,
+} from "./repository"
+import { GetNumberTypeConfigsSchema } from "./validation"
+// 특정 Number Type의 Configs 조회 (고급 필터링 지원)
+export async function getNumberTypeConfigs(input: GetNumberTypeConfigsSchema) {
+ return unstable_cache(
+ async () => {
+ try {
+ const offset = (input.page - 1) * input.perPage
+ // 고급 필터링 지원
+ const advancedWhere = filterColumns({
+ table: documentNumberTypeConfigs,
+ filters: input.filters,
+ joinOperator: input.joinOperator,
+ })
-// 특정 Number Type의 Configs 조회
-export async function getNumberTypeConfigs(numberTypeId: number) {
- console.log("=== getNumberTypeConfigs START ===")
- console.log("getNumberTypeConfigs called with numberTypeId:", numberTypeId)
-
- try {
- console.log("About to execute database query...")
-
- const configs = await db
- .select({
- id: documentNumberTypeConfigs.id,
- documentNumberTypeId: documentNumberTypeConfigs.documentNumberTypeId,
- codeGroupId: documentNumberTypeConfigs.codeGroupId,
- documentClassId: documentNumberTypeConfigs.documentClassId,
- sdq: documentNumberTypeConfigs.sdq,
- description: documentNumberTypeConfigs.description,
- remark: documentNumberTypeConfigs.remark,
- isActive: documentNumberTypeConfigs.isActive,
- createdAt: documentNumberTypeConfigs.createdAt,
- updatedAt: documentNumberTypeConfigs.updatedAt,
- // Code Group 정보도 함께 가져오기
- codeGroupName: codeGroups.description,
- codeGroupControlType: codeGroups.controlType,
- // Document Class 정보도 함께 가져오기
- documentClassName: documentClasses.value,
- documentClassDescription: documentClasses.description,
- })
- .from(documentNumberTypeConfigs)
- .leftJoin(codeGroups, eq(documentNumberTypeConfigs.codeGroupId, codeGroups.id))
- .leftJoin(documentClasses, eq(documentNumberTypeConfigs.documentClassId, documentClasses.id))
- .where(eq(documentNumberTypeConfigs.documentNumberTypeId, numberTypeId))
- .orderBy(asc(documentNumberTypeConfigs.sdq))
+ // 정렬 처리
+ const orderBy = input.sort.length > 0
+ ? input.sort
+ .map((item) => {
+ // 안전성 체크: 필드가 실제 테이블에 존재하는지 확인
+ if (!item || !item.id || typeof item.id !== "string" || !(item.id in documentNumberTypeConfigs)) {
+ return null;
+ }
+ const col = documentNumberTypeConfigs[item.id as keyof typeof documentNumberTypeConfigs];
+ return item.desc ? desc(col) : asc(col);
+ })
+ .filter((v): v is Exclude<typeof v, null> => v !== null)
+ : [asc(documentNumberTypeConfigs.sdq)]
-
+ // 트랜잭션 내부에서 Repository 호출
+ const { data, total } = await db.transaction(async (tx) => {
+ const data = await selectNumberTypeConfigs(tx, {
+ numberTypeId: input.numberTypeId,
+ where: advancedWhere,
+ orderBy,
+ offset,
+ limit: input.perPage,
+ search: input.search || undefined,
+ })
- return {
- success: true,
- data: configs,
- }
- } catch (error) {
-
- return {
- success: false,
- error: "Failed to fetch number type configs",
- data: [],
+ const total = await countNumberTypeConfigs(tx, {
+ numberTypeId: input.numberTypeId,
+ where: advancedWhere,
+ search: input.search || undefined,
+ })
+
+ return { data, total }
+ })
+
+ const pageCount = Math.ceil(total / input.perPage)
+
+ return {
+ success: true,
+ data,
+ pageCount,
+ }
+ } catch (error) {
+ console.error("Error fetching number type configs:", error)
+ return {
+ success: false,
+ error: "Failed to fetch number type configs",
+ data: [],
+ pageCount: 0,
+ }
+ }
+ },
+ [JSON.stringify(input)], // 캐싱 키
+ {
+ revalidate: 300, // 5분 캐시
+ tags: ["number-type-configs"],
}
- }
+ )()
}
// Number Type Config 생성
@@ -68,23 +104,16 @@ export async function createNumberTypeConfig(input: {
remark?: string
}) {
try {
- const [newConfig] = await db
- .insert(documentNumberTypeConfigs)
- .values({
- documentNumberTypeId: input.documentNumberTypeId,
- codeGroupId: input.codeGroupId,
- documentClassId: input.documentClassId,
- sdq: input.sdq,
- description: input.description,
- remark: input.remark,
- })
- .returning({ id: documentNumberTypeConfigs.id })
+ const result = await db.transaction(async (tx) => {
+ const [newConfig] = await insertNumberTypeConfig(tx, input)
+ return newConfig
+ })
revalidatePath("/evcp/docu-list-rule/number-type-configs")
return {
success: true,
- data: newConfig,
+ data: result,
message: "Number Type Config created successfully"
}
} catch (error) {
@@ -106,24 +135,22 @@ export async function updateNumberTypeConfig(input: {
remark?: string
}) {
try {
- const [updatedConfig] = await db
- .update(documentNumberTypeConfigs)
- .set({
+ const result = await db.transaction(async (tx) => {
+ const [updatedConfig] = await updateNumberTypeConfigById(tx, input.id, {
codeGroupId: input.codeGroupId,
documentClassId: input.documentClassId,
sdq: input.sdq,
description: input.description,
remark: input.remark,
- updatedAt: new Date(),
})
- .where(eq(documentNumberTypeConfigs.id, input.id))
- .returning({ id: documentNumberTypeConfigs.id })
+ return updatedConfig
+ })
revalidatePath("/evcp/docu-list-rule/number-type-configs")
return {
success: true,
- data: updatedConfig,
+ data: result,
message: "Number Type Config updated successfully"
}
} catch (error) {
@@ -138,37 +165,24 @@ export async function updateNumberTypeConfig(input: {
// Number Type Config 삭제
export async function deleteNumberTypeConfig(id: number) {
try {
- // 삭제할 항목의 정보를 먼저 가져옴
- const [configToDelete] = await db
- .select({
- documentNumberTypeId: documentNumberTypeConfigs.documentNumberTypeId,
- sdq: documentNumberTypeConfigs.sdq,
- })
- .from(documentNumberTypeConfigs)
- .where(eq(documentNumberTypeConfigs.id, id))
+ await db.transaction(async (tx) => {
+ // 삭제할 항목의 정보를 먼저 가져옴
+ const [configToDelete] = await selectConfigToDelete(tx, id)
- if (!configToDelete) {
- return {
- success: false,
- error: "Config not found"
+ if (!configToDelete) {
+ throw new Error("Config not found")
}
- }
- // 항목 삭제
- await db
- .delete(documentNumberTypeConfigs)
- .where(eq(documentNumberTypeConfigs.id, id))
+ // 항목 삭제
+ await deleteNumberTypeConfigById(tx, id)
- // 같은 Number Type의 남은 항목들 중에서 삭제된 항목보다 큰 순서를 가진 항목들의 순서를 1씩 감소
- await db
- .update(documentNumberTypeConfigs)
- .set({
- sdq: sql`${documentNumberTypeConfigs.sdq} - 1`,
- updatedAt: new Date(),
- })
- .where(
- sql`${documentNumberTypeConfigs.documentNumberTypeId} = ${configToDelete.documentNumberTypeId} AND ${documentNumberTypeConfigs.sdq} > ${configToDelete.sdq}`
+ // 같은 Number Type의 남은 항목들 중에서 삭제된 항목보다 큰 순서를 가진 항목들의 순서를 1씩 감소
+ await reorderConfigsAfterDelete(
+ tx,
+ configToDelete.documentNumberTypeId,
+ configToDelete.sdq
)
+ })
revalidatePath("/evcp/docu-list-rule/number-type-configs")
@@ -178,6 +192,14 @@ export async function deleteNumberTypeConfig(id: number) {
}
} catch (error) {
console.error("Error deleting number type config:", error)
+
+ if (error instanceof Error && error.message === "Config not found") {
+ return {
+ success: false,
+ error: "Config not found"
+ }
+ }
+
return {
success: false,
error: "Failed to delete number type config"
@@ -187,22 +209,10 @@ export async function deleteNumberTypeConfig(id: number) {
// 활성화된 Code Groups 조회 (Config 생성/수정 시 사용)
export async function getActiveCodeGroups() {
+ unstable_noStore()
+
try {
- console.log("getActiveCodeGroups: 함수 시작")
-
- const codeGroupsData = await db
- .select({
- id: codeGroups.id,
- groupId: codeGroups.groupId,
- description: codeGroups.description,
- controlType: codeGroups.controlType,
- isActive: codeGroups.isActive,
- })
- .from(codeGroups)
- .where(eq(codeGroups.isActive, true))
- .orderBy(asc(codeGroups.description))
-
- console.log("getActiveCodeGroups: 쿼리 결과", codeGroupsData)
+ const codeGroupsData = await selectActiveCodeGroups(db)
return {
success: true,
@@ -221,12 +231,7 @@ export async function getActiveCodeGroups() {
// Document Class Code Group ID 조회
export async function getDocumentClassCodeGroupId() {
try {
- const [codeGroup] = await db
- .select({ id: codeGroups.id })
- .from(codeGroups)
- .where(eq(codeGroups.groupId, 'DOC_CLASS'))
- .limit(1)
-
+ const [codeGroup] = await selectDocumentClassCodeGroupId(db)
return codeGroup?.id || null
} catch (error) {
console.error("Error fetching document class code group id:", error)
@@ -236,23 +241,20 @@ export async function getDocumentClassCodeGroupId() {
// 활성화된 Document Classes 조회 (Config 생성/수정 시 사용)
export async function getActiveDocumentClasses() {
+ unstable_noStore()
+
try {
- console.log("getActiveDocumentClasses: 함수 시작")
-
const documentClassesData = await db
.select({
id: documentClasses.id,
code: documentClasses.code,
value: documentClasses.value,
description: documentClasses.description,
- isActive: documentClasses.isActive,
})
.from(documentClasses)
.where(eq(documentClasses.isActive, true))
.orderBy(asc(documentClasses.value))
- console.log("getActiveDocumentClasses: 쿼리 결과", documentClassesData)
-
return {
success: true,
data: documentClassesData,
diff --git a/lib/docu-list-rule/number-type-configs/table/drag-drop-table.tsx b/lib/docu-list-rule/number-type-configs/table/drag-drop-table.tsx
new file mode 100644
index 00000000..3141e450
--- /dev/null
+++ b/lib/docu-list-rule/number-type-configs/table/drag-drop-table.tsx
@@ -0,0 +1,193 @@
+"use client"
+
+import * as React from "react"
+import { flexRender } from "@tanstack/react-table"
+import {
+ DndContext,
+ closestCenter,
+ KeyboardSensor,
+ PointerSensor,
+ useSensor,
+ useSensors,
+ DragEndEvent,
+} from '@dnd-kit/core'
+import {
+ SortableContext,
+ sortableKeyboardCoordinates,
+ verticalListSortingStrategy,
+} from '@dnd-kit/sortable'
+import {
+ useSortable,
+} from '@dnd-kit/sortable'
+import { CSS } from '@dnd-kit/utilities'
+
+import { cn } from "@/lib/utils"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { DataTablePagination } from "@/components/data-table/data-table-pagination"
+
+// 드래그 가능한 행 컴포넌트
+function SortableRow({
+ children,
+ id,
+ isDragging
+}: {
+ children: React.ReactNode
+ id: string
+ isDragging: boolean
+}) {
+ const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id })
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ opacity: isDragging ? 0.5 : 1,
+ }
+
+ return (
+ <TableRow
+ ref={setNodeRef}
+ style={style}
+ className={cn(
+ "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
+ isDragging && "opacity-50"
+ )}
+ >
+ <TableCell className="w-8 p-1 text-center">
+ <div
+ {...attributes}
+ {...listeners}
+ className="cursor-grab active:cursor-grabbing inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-6 w-6"
+ >
+ ⋮⋮
+ </div>
+ </TableCell>
+ {children}
+ </TableRow>
+ )
+}
+
+// 커스텀 드래그 앤 드롭 테이블 컴포넌트
+interface DragDropTableProps<TData> {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ table: any
+ data: TData[]
+ onDragEnd: (event: DragEndEvent) => void
+ children?: React.ReactNode
+ className?: string
+ maxHeight?: string
+}
+
+export function DragDropTable<TData>({
+ table,
+ data,
+ onDragEnd,
+ children,
+ className,
+ maxHeight = '35rem'
+}: DragDropTableProps<TData>) {
+ const sensors = useSensors(
+ useSensor(PointerSensor),
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
+ )
+
+ return (
+ <div className={cn("w-full space-y-2.5 overflow-auto", className)}>
+ {children}
+ <div className="max-w-[100vw] overflow-auto" style={{ maxHeight }}>
+ <DndContext
+ sensors={sensors}
+ collisionDetection={closestCenter}
+ onDragEnd={onDragEnd}
+ >
+ <Table className="[&>thead]:sticky [&>thead]:top-0 [&>thead]:z-10 table-fixed">
+ <TableHeader>
+ <TableRow>
+ <TableHead className="w-8 p-1 text-center"></TableHead>
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
+ {table.getHeaderGroups()[0].headers.map((header: any) => {
+ if (header.column.getIsGrouped()) {
+ return null
+ }
+
+ return (
+ <TableHead
+ key={header.id}
+ colSpan={header.colSpan}
+ data-column-id={header.column.id}
+ className="h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]"
+ style={{
+ width: header.getSize(),
+ }}
+ >
+ {header.isPlaceholder
+ ? null
+ : flexRender(header.column.columnDef.header, header.getContext())}
+ </TableHead>
+ )
+ })}
+ </TableRow>
+ </TableHeader>
+
+ <TableBody>
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
+ <SortableContext items={data.map((item: any) => item.id.toString())} strategy={verticalListSortingStrategy}>
+ {table.getRowModel().rows?.length ? (
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ table.getRowModel().rows.map((row: any) => {
+ if (row.getIsGrouped()) {
+ return null
+ }
+
+ return (
+ <SortableRow key={row.id} id={row.id} isDragging={false}>
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
+ {row.getVisibleCells().map((cell: any) => {
+ if (cell.column.getIsGrouped()) {
+ return null
+ }
+
+ return (
+ <TableCell
+ key={cell.id}
+ data-column-id={cell.column.id}
+ className="p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]"
+ style={{
+ width: cell.column.getSize(),
+ }}
+ >
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ </TableCell>
+ )
+ })}
+ </SortableRow>
+ )
+ })
+ ) : (
+ <TableRow>
+ <TableCell
+ colSpan={table.getAllColumns().length + 1}
+ className="h-24 text-center"
+ >
+ No results.
+ </TableCell>
+ </TableRow>
+ )}
+ </SortableContext>
+ </TableBody>
+ </Table>
+ </DndContext>
+ </div>
+
+ <div className="flex flex-col gap-2.5">
+ <DataTablePagination table={table} />
+ </div>
+ </div>
+ )
+} \ No newline at end of file
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 d3215958..72ee2698 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
@@ -55,7 +55,7 @@ export function NumberTypeConfigsEditDialog({
React.useEffect(() => {
if (data) {
setFormData({
- codeGroupId: data.codeGroupId.toString(),
+ codeGroupId: data.codeGroupId?.toString() || "", // null 체크 추가
sdq: data.sdq.toString(),
description: data.description || "",
remark: data.remark || ""
@@ -100,6 +100,7 @@ export function NumberTypeConfigsEditDialog({
const result = await updateNumberTypeConfig({
id: data.id,
codeGroupId: parseInt(formData.codeGroupId),
+ documentClassId: data.documentClassId, // 누락된 필드 추가
sdq: newSdq,
description: formData.description || undefined,
remark: formData.remark || undefined,
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 89a92a88..3b3d0180 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
@@ -2,488 +2,91 @@
import * as React from "react"
import { useDataTable } from "@/hooks/use-data-table"
-import { flexRender } from "@tanstack/react-table"
import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
import type { DataTableAdvancedFilterField, DataTableRowAction } from "@/types/table"
+import { arrayMove } from '@dnd-kit/sortable'
+import { DragEndEvent } from '@dnd-kit/core'
+import { toast } from "sonner"
-import { getNumberTypeConfigs, createNumberTypeConfig, getActiveCodeGroups, getActiveDocumentClasses, getDocumentClassCodeGroupId, updateNumberTypeConfig } from "../service"
-import { getNumberTypes } from "@/lib/docu-list-rule/number-types/service"
+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 { documentNumberTypes } from "@/db/schema/docu-list-rule"
-import { Plus, Loader2 } from "lucide-react"
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
-import { Textarea } from "@/components/ui/textarea"
-import { toast } from "sonner"
-import {
- DndContext,
- closestCenter,
- KeyboardSensor,
- PointerSensor,
- useSensor,
- useSensors,
- DragEndEvent,
-} from '@dnd-kit/core'
-import {
- arrayMove,
- SortableContext,
- sortableKeyboardCoordinates,
- verticalListSortingStrategy,
-} from '@dnd-kit/sortable'
-import {
- useSortable,
-} from '@dnd-kit/sortable'
-import { CSS } from '@dnd-kit/utilities'
-
-import { cn } from "@/lib/utils"
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table"
-import { DataTablePagination } from "@/components/data-table/data-table-pagination"
import { NumberTypeConfig } from "../../types"
+import { GetNumberTypeConfigsSchema } from "../validation"
interface NumberTypeConfigsTableProps {
promises?: Promise<[{ data: typeof documentNumberTypes.$inferSelect[]; pageCount: number }]>
+ searchParams: GetNumberTypeConfigsSchema
}
-// 드래그 가능한 행 컴포넌트
-function SortableRow({
- children,
- id,
- isDragging
-}: {
- children: React.ReactNode
- id: string
- isDragging: boolean
-}) {
- const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id })
-
- const style = {
- transform: CSS.Transform.toString(transform),
- transition,
- opacity: isDragging ? 0.5 : 1,
- }
-
- return (
- <TableRow
- ref={setNodeRef}
- style={style}
- className={cn(
- "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
- isDragging && "opacity-50"
- )}
- >
- <TableCell className="w-8 p-1 text-center">
- <div
- {...attributes}
- {...listeners}
- className="cursor-grab active:cursor-grabbing flex items-center justify-center w-5 h-5 text-muted-foreground hover:text-foreground"
- >
- <span className="text-sm">≡</span>
- </div>
- </TableCell>
- {children}
- </TableRow>
- )
-}
-
-// 커스텀 드래그 앤 드롭 테이블 컴포넌트
-function DragDropTable<TData>({
- table,
- data,
- onDragEnd,
- children,
- className,
- maxHeight = '35rem'
-}: {
- table: any
- data: TData[]
- onDragEnd: (event: DragEndEvent) => void
- children?: React.ReactNode
- className?: string
- maxHeight?: string
-}) {
- const sensors = useSensors(
- useSensor(PointerSensor),
- useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
- )
-
- return (
- <div className={cn("w-full space-y-2.5 overflow-auto", className)}>
- {children}
- <div className="max-w-[100vw] overflow-auto" style={{ maxHeight }}>
- <DndContext
- sensors={sensors}
- collisionDetection={closestCenter}
- onDragEnd={onDragEnd}
- >
- <Table className="[&>thead]:sticky [&>thead]:top-0 [&>thead]:z-10 table-fixed">
- <TableHeader>
- <TableRow>
- <TableHead className="w-8 p-1 text-center"></TableHead>
- {table.getHeaderGroups()[0].headers.map((header: any) => {
- if (header.column.getIsGrouped()) {
- return null
- }
-
- return (
- <TableHead
- key={header.id}
- colSpan={header.colSpan}
- data-column-id={header.column.id}
- className="h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]"
- style={{
- width: header.getSize(),
- }}
- >
- {header.isPlaceholder
- ? null
- : flexRender(header.column.columnDef.header, header.getContext())}
- </TableHead>
- )
- })}
- </TableRow>
- </TableHeader>
-
- <TableBody>
- <SortableContext items={data.map((item: any) => item.id.toString())} strategy={verticalListSortingStrategy}>
- {table.getRowModel().rows?.length ? (
- table.getRowModel().rows.map((row: any) => {
- if (row.getIsGrouped()) {
- return null
- }
-
- return (
- <SortableRow key={row.id} id={row.id} isDragging={false}>
- {row.getVisibleCells().map((cell: any) => {
- if (cell.column.getIsGrouped()) {
- return null
- }
-
- return (
- <TableCell
- key={cell.id}
- data-column-id={cell.column.id}
- className="p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]"
- style={{
- width: cell.column.getSize(),
- }}
- >
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
- </TableCell>
- )
- })}
- </SortableRow>
- )
- })
- ) : (
- <TableRow>
- <TableCell
- colSpan={table.getAllColumns().length + 1}
- className="h-24 text-center"
- >
- No results.
- </TableCell>
- </TableRow>
- )}
- </SortableContext>
- </TableBody>
- </Table>
- </DndContext>
- </div>
-
- <div className="flex flex-col gap-2.5">
- <DataTablePagination table={table} />
- </div>
- </div>
- )
-}
-
-// 툴바 액션 컴포넌트 (incoterms와 동일한 스타일)
-function NumberTypeConfigsTableToolbarActions({
- table,
- onSuccess,
- selectedNumberType,
- configsData
-}: {
- table: any
- onSuccess?: () => void
- selectedNumberType: number | null
- configsData: NumberTypeConfig[]
-}) {
- const [isAddDialogOpen, setIsAddDialogOpen] = React.useState(false)
- const [isLoading, setIsLoading] = React.useState(false)
- const [formData, setFormData] = React.useState({ codeGroupId: "", description: "", remark: "" })
- const [codeGroups, setCodeGroups] = React.useState<{ id: number; description: string }[]>([])
- const [allOptions, setAllOptions] = React.useState<{ id: string; name: string }[]>([])
+export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeConfigsTableProps) {
+ const rawData = React.use(promises!)
+ 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 loadCodeGroups = React.useCallback(async () => {
+ // configs 데이터 로드 함수
+ const fetchConfigs = React.useCallback(async (numberTypeId: number, params?: Partial<GetNumberTypeConfigsSchema>) => {
try {
- console.log("NumberTypeConfigsTableToolbarActions: Code Groups 로딩 시작")
- console.log("NumberTypeConfigsTableToolbarActions: getActiveCodeGroups 함수 호출")
- const result = await getActiveCodeGroups()
- console.log("NumberTypeConfigsTableToolbarActions: getActiveCodeGroups 결과", result)
- console.log("NumberTypeConfigsTableToolbarActions: result.success", result.success)
- console.log("NumberTypeConfigsTableToolbarActions: result.data", result.data)
- console.log("NumberTypeConfigsTableToolbarActions: result.error", result.error)
-
+ const result = await getNumberTypeConfigs({
+ ...searchParams,
+ ...params,
+ numberTypeId,
+ })
if (result.success && result.data) {
- console.log("NumberTypeConfigsTableToolbarActions: Code Groups 설정", result.data)
-
- // 이미 추가된 Code Group들을 제외하고 필터링
- const usedCodeGroupIds = configsData.map(config => config.codeGroupId)
- const availableCodeGroups = result.data.filter(codeGroup =>
- !usedCodeGroupIds.includes(codeGroup.id)
- )
-
- console.log("NumberTypeConfigsTableToolbarActions: 사용된 Code Group IDs", usedCodeGroupIds)
- console.log("NumberTypeConfigsTableToolbarActions: 사용 가능한 Code Groups", availableCodeGroups)
-
- setCodeGroups(availableCodeGroups)
+ setConfigsData({ data: result.data, pageCount: result.pageCount })
} else {
- console.error("NumberTypeConfigsTableToolbarActions: Code Groups 로딩 실패", result.error)
+ setConfigsData({ data: [], pageCount: 0 })
}
} catch (error) {
- console.error("Error loading code groups:", error)
- console.error("Error details:", error)
+ console.error("Error fetching configs:", error)
+ setConfigsData({ data: [], pageCount: 0 })
}
- }, [configsData])
-
-
+ }, [searchParams])
+ // Number Types 데이터 로드 및 첫 번째 타입의 configs 데이터 로드
React.useEffect(() => {
- loadCodeGroups()
- }, [loadCodeGroups])
-
- // Code Groups를 옵션 목록으로 만드는 함수
- const combineOptions = React.useCallback(() => {
- const codeGroupOptions = codeGroups.map(cg => ({
- id: `cg_${cg.id}`,
- name: cg.description
- }))
-
- setAllOptions(codeGroupOptions)
- }, [codeGroups])
-
- // Code Groups가 변경될 때마다 옵션 목록 업데이트
- React.useEffect(() => {
- combineOptions()
- }, [combineOptions])
-
- // 다이얼로그가 열릴 때마다 Code Groups 다시 로드
- React.useEffect(() => {
- if (isAddDialogOpen) {
- loadCodeGroups()
- }
- }, [isAddDialogOpen, loadCodeGroups, configsData])
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- if (!selectedNumberType || !formData.codeGroupId) {
- toast.error("필수 필드를 모두 입력해주세요.")
- return
- }
-
- const sdq = getNextSdq()
- setIsLoading(true)
-
- try {
- // Code Group ID 추출
- const codeGroupId = parseInt(formData.codeGroupId.replace('cg_', ''))
-
- const result = await createNumberTypeConfig({
- documentNumberTypeId: selectedNumberType,
- codeGroupId: codeGroupId,
- documentClassId: null,
- sdq: sdq,
- description: formData.description || undefined,
- remark: formData.remark || undefined,
- })
-
- if (result.success) {
- toast.success("Number Type Config가 성공적으로 추가되었습니다.")
- setIsAddDialogOpen(false)
- setFormData({ codeGroupId: "", description: "", remark: "" })
- onSuccess?.()
- } else {
- toast.error(result.error || "추가에 실패했습니다.")
+ const loadData = async () => {
+ try {
+ const result = rawData[0]
+
+ if (result.data && result.data.length > 0) {
+ const firstNumberTypeId = result.data[0].id
+ setSelectedNumberType(firstNumberTypeId)
+
+ // 첫 번째 타입의 configs 데이터도 바로 로드
+ await fetchConfigs(firstNumberTypeId)
+ }
+ } catch (error) {
+ console.error("Error in loadData:", error)
}
- } catch (error) {
- console.error("Error creating number type config:", error)
- toast.error("추가 중 오류가 발생했습니다.")
- } finally {
- setIsLoading(false)
}
- }
-
- const getNextSdq = () => {
- if (configsData.length === 0) return 1
- const maxSdq = Math.max(...configsData.map(config => config.sdq))
- return maxSdq + 1
- }
-
- return (
- <div className="flex items-center gap-2">
- {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */}
- {table.getFilteredSelectedRowModel().rows.length > 0 ? (
- <DeleteNumberTypeConfigsDialog
- configs={table
- .getFilteredSelectedRowModel()
- .rows.map((row: any) => row.original)}
- onSuccess={() => {
- table.toggleAllRowsSelected(false);
- onSuccess?.();
- }}
- />
- ) : null}
-
- <Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
- <DialogTrigger asChild>
- <Button variant="outline" size="sm" disabled={!selectedNumberType || codeGroups.length === 0}>
- <Plus className="mr-2 h-4 w-4" />
- Add
- </Button>
- </DialogTrigger>
- <DialogContent className="max-w-md">
- <DialogHeader>
- <DialogTitle>Number Type Config 추가</DialogTitle>
- <DialogDescription>
- 새로운 구성 요소를 추가합니다. 필수 정보를 입력해주세요.
- <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span>
- </DialogDescription>
- </DialogHeader>
- <form onSubmit={handleSubmit} className="space-y-4">
- <div className="grid gap-4 py-2">
- <div className="grid grid-cols-4 items-center gap-4">
- <Label htmlFor="codeGroup" className="text-right">
- Code Group <span className="text-red-500">*</span>
- </Label>
- <div className="col-span-3">
- <Select
- value={formData.codeGroupId}
- onValueChange={(value) => setFormData(prev => ({ ...prev, codeGroupId: value }))}
- >
- <SelectTrigger>
- <SelectValue placeholder="Code Group 선택" />
- </SelectTrigger>
- <SelectContent>
- {allOptions.length > 0 ? (
- allOptions.map((option) => (
- <SelectItem key={option.id} value={option.id}>
- {option.name}
- </SelectItem>
- ))
- ) : (
- <div className="px-2 py-1.5 text-sm text-muted-foreground">
- 사용 가능한 옵션이 없습니다.
- </div>
- )}
- </SelectContent>
- </Select>
- </div>
- </div>
- <div className="grid grid-cols-4 items-center gap-4">
- <Label htmlFor="description" className="text-right">
- Description
- </Label>
- <div className="col-span-3">
- <Input
- id="description"
- value={formData.description}
- onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
- placeholder="예: PROJECT NO"
- />
- </div>
- </div>
- <div className="grid grid-cols-4 items-center gap-4">
- <Label htmlFor="remark" className="text-right">
- Remark
- </Label>
- <div className="col-span-3">
- <Textarea
- id="remark"
- value={formData.remark}
- onChange={(e) => setFormData(prev => ({ ...prev, remark: e.target.value }))}
- placeholder="비고 사항"
- rows={3}
- />
- </div>
- </div>
- </div>
- <DialogFooter>
- <Button
- type="button"
- variant="outline"
- onClick={() => setIsAddDialogOpen(false)}
- disabled={isLoading}
- >
- 취소
- </Button>
- <Button
- type="submit"
- disabled={isLoading}
- >
- {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
- {isLoading ? "추가 중..." : "추가"}
- </Button>
- </DialogFooter>
- </form>
- </DialogContent>
- </Dialog>
- </div>
- )
-}
-
-export function NumberTypeConfigsTable({ promises }: NumberTypeConfigsTableProps) {
- const rawData = React.use(promises!)
- const [selectedNumberType, setSelectedNumberType] = React.useState<number | null>(null)
- const [configsData, setConfigsData] = React.useState<NumberTypeConfig[]>([])
- const [rowAction, setRowAction] = React.useState<DataTableRowAction<NumberTypeConfig> | null>(null)
-
- // 상태 변화 추적
- React.useEffect(() => {
- console.log("selectedNumberType changed:", selectedNumberType)
- }, [selectedNumberType])
+
+ loadData()
+ }, [rawData, fetchConfigs])
- React.useEffect(() => {
- console.log("configsData changed:", configsData)
- console.log("configsData length:", configsData.length)
- }, [configsData])
+ // Number Type 변경 핸들러 (서버 사이드 처리 지원)
+ const handleNumberTypeChange = React.useCallback((numberTypeId: number) => {
+ setSelectedNumberType(numberTypeId)
+ // searchParams를 업데이트하여 서버 사이드 필터링 적용
+ fetchConfigs(numberTypeId, { page: 1 }) // 페이지 리셋
+ }, [fetchConfigs])
// 드래그 종료 핸들러
- const handleDragEnd = async (event: DragEndEvent) => {
+ const handleDragEnd = React.useCallback(async (event: DragEndEvent) => {
const { active, over } = event
if (active.id !== over?.id) {
- const oldIndex = configsData.findIndex(config => config.id.toString() === active.id)
- const newIndex = configsData.findIndex(config => config.id.toString() === over?.id)
+ const oldIndex = configsData.data.findIndex(config => config.id.toString() === active.id)
+ const newIndex = configsData.data.findIndex(config => config.id.toString() === over?.id)
if (oldIndex !== -1 && newIndex !== -1) {
- const newConfigsData = arrayMove(configsData, oldIndex, newIndex)
+ const newConfigsData = arrayMove(configsData.data, oldIndex, newIndex)
// 순서 업데이트
const updatedConfigsData = newConfigsData.map((config, index) => ({
@@ -491,7 +94,7 @@ export function NumberTypeConfigsTable({ promises }: NumberTypeConfigsTableProps
sdq: index + 1
}))
- setConfigsData(updatedConfigsData)
+ setConfigsData(prev => ({ ...prev, data: updatedConfigsData }))
// 서버에 순서 업데이트
try {
@@ -511,92 +114,12 @@ export function NumberTypeConfigsTable({ promises }: NumberTypeConfigsTableProps
toast.error("순서 변경 중 오류가 발생했습니다.")
// 에러 시 원래 데이터로 복원
if (selectedNumberType) {
- const refreshResult = await getNumberTypeConfigs(selectedNumberType)
- if (refreshResult.success && refreshResult.data) setConfigsData(refreshResult.data)
+ await fetchConfigs(selectedNumberType)
}
}
}
}
- }
-
- // Number Type Configs를 가져오는 함수
- const fetchConfigs = React.useCallback(async (numberTypeId: number) => {
- try {
- const result = await getNumberTypeConfigs(numberTypeId)
- if (result.success && result.data) {
- console.log("Configs data loaded:", result.data)
- setConfigsData(result.data)
- }
- } catch (error) {
- console.error("Error loading configs:", error)
- }
- }, [])
-
- // Number Types 데이터 로드 및 첫 번째 타입의 configs 데이터 로드
- React.useEffect(() => {
- const loadData = async () => {
- console.log("useEffect triggered - rawData:", !!rawData)
-
- try {
- const result = rawData[0]
- console.log("Raw data result:", result)
-
- if (result.data && result.data.length > 0) {
- const firstNumberTypeId = result.data[0].id
- console.log("Setting first number type ID:", firstNumberTypeId)
- setSelectedNumberType(firstNumberTypeId)
-
- // 첫 번째 타입의 configs 데이터도 바로 로드
- console.log("Loading configs for first number type:", firstNumberTypeId)
- const configsResult = await getNumberTypeConfigs(firstNumberTypeId)
- console.log("Configs result received:", configsResult)
-
- if (configsResult && configsResult.success && configsResult.data) {
- console.log("Setting configs data:", configsResult.data)
- setConfigsData(configsResult.data)
- } else {
- console.log("Configs result not successful or no data:", configsResult)
- setConfigsData([])
- }
- }
- } catch (error) {
- console.error("Error in loadData:", error)
- }
- }
-
- loadData()
- }, [rawData])
-
- // selectedNumberType이 변경될 때 configs 데이터 로드 (첫 번째 로드 이후)
- React.useEffect(() => {
- console.log("Second useEffect triggered - selectedNumberType:", selectedNumberType)
-
- const loadConfigs = async () => {
- if (selectedNumberType) {
- console.log("Loading configs for selectedNumberType:", selectedNumberType)
-
- try {
- const configsResult = await getNumberTypeConfigs(selectedNumberType)
- console.log("Configs result received:", configsResult)
-
- if (configsResult && configsResult.success && configsResult.data) {
- console.log("Setting configs data:", configsResult.data)
- setConfigsData(configsResult.data)
- } else {
- console.log("Configs result not successful or no data:", configsResult)
- setConfigsData([])
- }
- } catch (error) {
- console.error("Error loading configs:", error)
- setConfigsData([])
- }
- } else {
- console.log("selectedNumberType is null, skipping configs load")
- }
- }
-
- loadConfigs()
- }, [selectedNumberType])
+ }, [configsData.data, selectedNumberType, fetchConfigs])
// advanced filter fields 정의
const advancedFilterFields: DataTableAdvancedFilterField<NumberTypeConfig>[] = [
@@ -607,31 +130,23 @@ export function NumberTypeConfigsTable({ promises }: NumberTypeConfigsTableProps
{ id: "isActive", label: "상태", type: "select", options: [ { label: "활성", value: "true"}, { label: "비활성", value: "false" }] },
]
- // useDataTable 적용
+ // useDataTable 적용 (서버 사이드 처리)
const columns = React.useMemo(() => {
const cols = getColumns({ setRowAction })
- console.log("Generated columns:", cols.map(col => (col as any).id || (col as any).accessorKey))
return cols
}, [setRowAction])
- // 클라이언트 사이드 정렬을 위한 정렬된 데이터
- const sortedConfigsData = React.useMemo(() => {
- return [...configsData].sort((a, b) => {
- // 기본적으로 sdq 순으로 정렬
- return a.sdq - b.sdq
- })
- }, [configsData])
-
const { table } = useDataTable({
- data: sortedConfigsData,
+ data: configsData.data,
columns: columns,
- pageCount: 1,
+ pageCount: configsData.pageCount,
enablePinning: true,
enableAdvancedFilter: true,
- manualSorting: false,
+ manualSorting: true,
+ manualFiltering: true,
+ manualPagination: true,
initialState: {
sorting: [{ id: "sdq", desc: false }],
- // columnPinning: { right: ["actions"] }, // 일시적으로 제거
},
getRowId: (row) => String(row.id),
shallow: false,
@@ -640,80 +155,75 @@ export function NumberTypeConfigsTable({ promises }: NumberTypeConfigsTableProps
const refreshData = React.useCallback(async () => {
if (selectedNumberType) {
- try {
- const result = await getNumberTypeConfigs(selectedNumberType)
- if (result.success && result.data) setConfigsData(result.data)
- } catch (error) {
- console.error("Error refreshing data:", error)
- }
+ await fetchConfigs(selectedNumberType)
}
- }, [selectedNumberType])
+ }, [selectedNumberType, fetchConfigs])
+
+ // 테이블 상태가 변경될 때 서버 데이터 다시 로드
+ React.useEffect(() => {
+ if (selectedNumberType) {
+ const state = table.getState()
+ fetchConfigs(selectedNumberType, {
+ page: state.pagination.pageIndex + 1,
+ perPage: state.pagination.pageSize,
+ sort: state.sorting.map(s => ({ id: s.id as keyof NumberTypeConfig, desc: s.desc })),
+ search: state.globalFilter || "",
+ })
+ }
+ }, [table, selectedNumberType, fetchConfigs])
return (
<>
{/* Number Type 선택 */}
- <div className="mb-6">
- <label className="text-sm font-medium mb-2 block">Number Type 선택</label>
- <div className="flex gap-2">
- {rawData[0]?.data && rawData[0].data.length > 0 ? (
- rawData[0].data.map((numberType) => (
- <button
- key={numberType.id}
- onClick={() => {
- setSelectedNumberType(numberType.id)
- fetchConfigs(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="px-4 py-2 text-sm text-muted-foreground">
- Number Type을 불러오는 중...
- </div>
- )}
- </div>
- </div>
+ <NumberTypeSelector
+ numberTypes={rawData[0]?.data || []}
+ selectedNumberType={selectedNumberType}
+ onNumberTypeChange={handleNumberTypeChange}
+ isLoading={!rawData[0]?.data}
+ />
{/* 선택된 Number Type 정보 및 테이블: 조건부 렌더링 */}
{selectedNumberType && rawData[0]?.data && rawData[0].data.length > 0 && (
- <>
- <DragDropTable
- table={table}
- data={configsData}
- onDragEnd={handleDragEnd}
+ <DragDropTable
+ table={table}
+ data={configsData.data}
+ onDragEnd={handleDragEnd}
+ >
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
>
- <DataTableAdvancedToolbar table={table} filterFields={advancedFilterFields}>
- <NumberTypeConfigsTableToolbarActions
- table={table}
- onSuccess={refreshData}
- selectedNumberType={selectedNumberType}
- configsData={configsData}
- />
- </DataTableAdvancedToolbar>
- </DragDropTable>
- </>
+ <NumberTypeConfigsToolbarActions
+ table={table}
+ onSuccess={refreshData}
+ selectedNumberType={selectedNumberType}
+ configsData={configsData.data}
+ />
+ </DataTableAdvancedToolbar>
+ </DragDropTable>
)}
{/* 삭제/수정 다이얼로그 */}
<DeleteNumberTypeConfigsDialog
open={rowAction?.type === "delete"}
onOpenChange={() => setRowAction(null)}
- configs={rowAction?.row ? [rowAction.row.original] : []}
+ configs={rowAction?.row.original ? [rowAction.row.original] : []}
showTrigger={false}
- onSuccess={refreshData}
+ onSuccess={() => {
+ setRowAction(null)
+ refreshData()
+ }}
/>
+
<NumberTypeConfigsEditDialog
open={rowAction?.type === "update"}
onOpenChange={() => setRowAction(null)}
- data={rowAction?.row?.original ?? null}
- existingConfigs={configsData}
- onSuccess={refreshData}
+ data={rowAction?.row.original ?? null}
+ existingConfigs={configsData.data}
+ 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
new file mode 100644
index 00000000..4923bd1d
--- /dev/null
+++ b/lib/docu-list-rule/number-type-configs/table/number-type-configs-toolbar-actions.tsx
@@ -0,0 +1,251 @@
+"use client"
+
+import * as React from "react"
+import { Plus, Loader2 } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { Input } from "@/components/ui/input"
+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"
+
+interface NumberTypeConfigsToolbarActionsProps {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ table: any
+ onSuccess?: () => void
+ selectedNumberType: number | null
+ configsData: NumberTypeConfig[]
+}
+
+export function NumberTypeConfigsToolbarActions({
+ table,
+ onSuccess,
+ selectedNumberType,
+ configsData
+}: NumberTypeConfigsToolbarActionsProps) {
+ const [isAddDialogOpen, setIsAddDialogOpen] = React.useState(false)
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [formData, setFormData] = React.useState({ codeGroupId: "", description: "", remark: "" })
+ const [codeGroups, setCodeGroups] = React.useState<{ id: number; description: string }[]>([])
+ const [allOptions, setAllOptions] = React.useState<{ id: string; name: string }[]>([])
+
+ const loadCodeGroups = React.useCallback(async () => {
+ try {
+ const result = await getActiveCodeGroups()
+ if (result.success && result.data) {
+
+ // 이미 추가된 Code Group들을 제외하고 필터링
+ const usedCodeGroupIds = configsData.map(config => config.codeGroupId)
+ const availableCodeGroups = result.data.filter(codeGroup =>
+ !usedCodeGroupIds.includes(codeGroup.id)
+ )
+ setCodeGroups(availableCodeGroups)
+ }
+ } catch (error) {
+ console.error("Error details:", error)
+ }
+ }, [configsData])
+
+ React.useEffect(() => {
+ loadCodeGroups()
+ }, [loadCodeGroups])
+
+ // Code Groups를 옵션 목록으로 만드는 함수
+ const combineOptions = React.useCallback(() => {
+ const codeGroupOptions = codeGroups.map(cg => ({
+ id: `cg_${cg.id}`,
+ name: cg.description
+ }))
+
+ setAllOptions(codeGroupOptions)
+ }, [codeGroups])
+
+ // Code Groups가 변경될 때마다 옵션 목록 업데이트
+ React.useEffect(() => {
+ combineOptions()
+ }, [combineOptions])
+
+ // 다이얼로그가 열릴 때마다 Code Groups 다시 로드
+ React.useEffect(() => {
+ if (isAddDialogOpen) {
+ loadCodeGroups()
+ }
+ }, [isAddDialogOpen, loadCodeGroups, configsData])
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!selectedNumberType || !formData.codeGroupId) {
+ toast.error("필수 필드를 모두 입력해주세요.")
+ return
+ }
+
+ const sdq = getNextSdq()
+ setIsLoading(true)
+
+ try {
+ // Code Group ID 추출
+ const codeGroupId = parseInt(formData.codeGroupId.replace('cg_', ''))
+
+ const result = await createNumberTypeConfig({
+ documentNumberTypeId: selectedNumberType,
+ codeGroupId: codeGroupId,
+ documentClassId: null,
+ sdq: sdq,
+ description: formData.description || undefined,
+ remark: formData.remark || undefined,
+ })
+
+ if (result.success) {
+ toast.success("Number Type Config가 성공적으로 추가되었습니다.")
+ setIsAddDialogOpen(false)
+ setFormData({ codeGroupId: "", description: "", remark: "" })
+ onSuccess?.()
+ } else {
+ toast.error(result.error || "추가에 실패했습니다.")
+ }
+ } catch (error) {
+ console.error("Error creating number type config:", error)
+ toast.error("추가 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const getNextSdq = () => {
+ if (configsData.length === 0) return 1
+ const maxSdq = Math.max(...configsData.map(config => config.sdq))
+ return maxSdq + 1
+ }
+
+ return (
+ <div className="flex items-center gap-2">
+ {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */}
+ {table.getFilteredSelectedRowModel().rows.length > 0 ? (
+ <DeleteNumberTypeConfigsDialog
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ configs={table
+ .getFilteredSelectedRowModel()
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ .rows.map((row: any) => row.original)}
+ onSuccess={() => {
+ table.toggleAllRowsSelected(false);
+ onSuccess?.();
+ }}
+ />
+ ) : null}
+
+ <Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm" disabled={!selectedNumberType || codeGroups.length === 0}>
+ <Plus className="mr-2 h-4 w-4" />
+ Add
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="max-w-md">
+ <DialogHeader>
+ <DialogTitle>Number Type Config 추가</DialogTitle>
+ <DialogDescription>
+ 새로운 구성 요소를 추가합니다. 필수 정보를 입력해주세요.
+ <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span>
+ </DialogDescription>
+ </DialogHeader>
+ <form onSubmit={handleSubmit} className="space-y-4">
+ <div className="grid gap-4 py-2">
+ <div className="grid grid-cols-4 items-center gap-4">
+ <Label htmlFor="codeGroup" className="text-right">
+ Code Group <span className="text-red-500">*</span>
+ </Label>
+ <div className="col-span-3">
+ <Select
+ value={formData.codeGroupId}
+ onValueChange={(value) => setFormData(prev => ({ ...prev, codeGroupId: value }))}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder="Code Group 선택" />
+ </SelectTrigger>
+ <SelectContent>
+ {allOptions.length > 0 ? (
+ allOptions.map((option) => (
+ <SelectItem key={option.id} value={option.id}>
+ {option.name}
+ </SelectItem>
+ ))
+ ) : (
+ <div className="px-2 py-1.5 text-sm text-muted-foreground">
+ 사용 가능한 옵션이 없습니다.
+ </div>
+ )}
+ </SelectContent>
+ </Select>
+ </div>
+ </div>
+ <div className="grid grid-cols-4 items-center gap-4">
+ <Label htmlFor="description" className="text-right">
+ Description
+ </Label>
+ <div className="col-span-3">
+ <Input
+ id="description"
+ value={formData.description}
+ onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
+ placeholder="예: PROJECT NO"
+ />
+ </div>
+ </div>
+ <div className="grid grid-cols-4 items-center gap-4">
+ <Label htmlFor="remark" className="text-right">
+ Remark
+ </Label>
+ <div className="col-span-3">
+ <Textarea
+ id="remark"
+ value={formData.remark}
+ onChange={(e) => setFormData(prev => ({ ...prev, remark: e.target.value }))}
+ placeholder="비고 사항"
+ rows={3}
+ />
+ </div>
+ </div>
+ </div>
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => setIsAddDialogOpen(false)}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button
+ type="submit"
+ disabled={isLoading}
+ >
+ {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ {isLoading ? "추가 중..." : "추가"}
+ </Button>
+ </DialogFooter>
+ </form>
+ </DialogContent>
+ </Dialog>
+ </div>
+ )
+} \ No newline at end of file
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
new file mode 100644
index 00000000..c311730e
--- /dev/null
+++ b/lib/docu-list-rule/number-type-configs/table/number-type-selector.tsx
@@ -0,0 +1,61 @@
+"use client"
+
+import * as React from "react"
+import { documentNumberTypes } from "@/db/schema/docu-list-rule"
+
+interface NumberTypeSelectorProps {
+ numberTypes: typeof documentNumberTypes.$inferSelect[]
+ selectedNumberType: number | null
+ onNumberTypeChange: (numberTypeId: number) => void
+ isLoading?: boolean
+}
+
+export function NumberTypeSelector({
+ numberTypes,
+ selectedNumberType,
+ onNumberTypeChange,
+ isLoading = false
+}: NumberTypeSelectorProps) {
+ 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>
+ </div>
+ )
+ }
+
+ 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>
+ </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>
+ </div>
+ )
+} \ No newline at end of file
diff --git a/lib/docu-list-rule/number-type-configs/validation.ts b/lib/docu-list-rule/number-type-configs/validation.ts
index 2b3aaa2d..02b99290 100644
--- a/lib/docu-list-rule/number-type-configs/validation.ts
+++ b/lib/docu-list-rule/number-type-configs/validation.ts
@@ -1,12 +1,30 @@
import { createSearchParamsCache } from "nuqs/server";
import { parseAsInteger, parseAsString, parseAsArrayOf, parseAsStringEnum } from "nuqs/server";
import { getSortingStateParser, getFiltersStateParser } from "@/lib/parsers";
+import * as z from "zod"
+import { NumberTypeConfig } from "../types";
export const searchParamsNumberTypeConfigsCache = createSearchParamsCache({
+ flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]),
page: parseAsInteger.withDefault(1),
perPage: parseAsInteger.withDefault(10),
- sort: getSortingStateParser<any>(),
- filters: getFiltersStateParser(),
- search: parseAsString.withDefault(""),
+ sort: getSortingStateParser<NumberTypeConfig>().withDefault([
+ { id: "sdq", desc: false },
+ ]),
+
+ // Number Type ID (필수)
+ numberTypeId: parseAsInteger.withDefault(0),
+
+ // 기본 필터들
+ codeGroupName: parseAsString.withDefault(""),
+ description: parseAsString.withDefault(""),
+ remark: parseAsString.withDefault(""),
+ isActive: parseAsString.withDefault(""),
+
+ // 고급 필터
+ filters: getFiltersStateParser().withDefault([]),
joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"),
-}); \ No newline at end of file
+ search: parseAsString.withDefault(""),
+});
+
+export type GetNumberTypeConfigsSchema = Awaited<ReturnType<typeof searchParamsNumberTypeConfigsCache.parse>> \ No newline at end of file
diff --git a/lib/docu-list-rule/number-types/service.ts b/lib/docu-list-rule/number-types/service.ts
index 8eaf19c7..17373848 100644
--- a/lib/docu-list-rule/number-types/service.ts
+++ b/lib/docu-list-rule/number-types/service.ts
@@ -22,7 +22,7 @@ export async function getNumberTypes(input: {
unstable_noStore()
try {
- const { page, perPage, sort, search } = input
+ const { page, perPage, sort, search, filters, joinOperator } = input
const offset = (page - 1) * perPage
// 기본 조건
@@ -37,30 +37,46 @@ export async function getNumberTypes(input: {
)`
}
- // 정렬
+ // 고급 필터링
+ if (filters && filters.length > 0) {
+ const filterConditions = filters.map(filter => {
+ const { id, value } = filter
+ if (!value) return null
+
+ switch (id) {
+ case "name":
+ return sql`${documentNumberTypes.name} ILIKE ${`%${value}%`}`
+ case "description":
+ return sql`${documentNumberTypes.description} ILIKE ${`%${value}%`}`
+ case "isActive":
+ return sql`${documentNumberTypes.isActive} = ${value === "true"}`
+ case "createdAt":
+ return sql`${documentNumberTypes.createdAt}::text ILIKE ${`%${value}%`}`
+ default:
+ return null
+ }
+ }).filter(Boolean)
+
+ if (filterConditions.length > 0) {
+ const operator = joinOperator === "or" ? sql` OR ` : sql` AND `
+ const combinedFilters = filterConditions.reduce((acc, condition, index) => {
+ if (index === 0) return condition
+ return sql`${acc}${operator}${condition}`
+ })
+
+ whereConditions = sql`${whereConditions} AND (${combinedFilters})`
+ }
+ }
+
+ // 정렬 (안전한 필드 체크 적용)
let orderBy = sql`${documentNumberTypes.createdAt} DESC`
if (sort && sort.length > 0) {
const sortField = sort[0]
- const direction = sortField.desc ? sql`DESC` : sql`ASC`
-
- switch (sortField.id) {
- case "id":
- orderBy = sql`${documentNumberTypes.id} ${direction}`
- break
- case "name":
- orderBy = sql`${documentNumberTypes.name} ${direction}`
- break
- case "description":
- orderBy = sql`${documentNumberTypes.description} ${direction}`
- break
- case "isActive":
- orderBy = sql`${documentNumberTypes.isActive} ${direction}`
- break
- case "createdAt":
- orderBy = sql`${documentNumberTypes.createdAt} ${direction}`
- break
- default:
- orderBy = sql`${documentNumberTypes.createdAt} DESC`
+ // 안전성 체크: 필드가 실제 테이블에 존재하는지 확인
+ if (sortField && sortField.id && typeof sortField.id === "string" && sortField.id in documentNumberTypes) {
+ const direction = sortField.desc ? sql`DESC` : sql`ASC`
+ const col = documentNumberTypes[sortField.id as keyof typeof documentNumberTypes]
+ orderBy = sql`${col} ${direction}`
}
}
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 5d25c4b5..00f7e2ab 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
@@ -1,6 +1,7 @@
"use client"
import * as React from "react"
+import { useRouter } from "next/navigation"
import { useDataTable } from "@/hooks/use-data-table"
import { DataTable } from "@/components/data-table/data-table"
import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
@@ -17,12 +18,13 @@ interface NumberTypesTableProps {
}
export function NumberTypesTable({ promises }: NumberTypesTableProps) {
+ const router = useRouter()
const rawData = React.use(promises!)
const [rowAction, setRowAction] = React.useState<DataTableRowAction<typeof documentNumberTypes.$inferSelect> | null>(null)
const refreshData = React.useCallback(() => {
- window.location.reload()
- }, [])
+ router.refresh()
+ }, [router])
const columns = React.useMemo(() => getColumns({ setRowAction }), [setRowAction])