summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
author0-Zz-ang <s1998319@gmail.com>2025-08-04 14:59:15 +0900
committer0-Zz-ang <s1998319@gmail.com>2025-08-04 14:59:15 +0900
commit59b5715ebb3e1fd7bd4eb02ce50399715734f865 (patch)
tree39ccd16482c1b90b6583ead73384822157254d88 /lib
parentf0213de0d2fb5fcb931b3ddaddcbb6581cab5d28 (diff)
(박서영) docu-list-rule detail sheet 컴포넌트 추가 및 검색 필터 기능 오류 수정
Diffstat (limited to 'lib')
-rw-r--r--lib/docu-list-rule/code-groups/service.ts10
-rw-r--r--lib/docu-list-rule/code-groups/table/code-groups-add-dialog.tsx14
-rw-r--r--lib/docu-list-rule/code-groups/table/code-groups-edit-sheet.tsx14
-rw-r--r--lib/docu-list-rule/code-groups/table/code-groups-table.tsx3
-rw-r--r--lib/docu-list-rule/code-groups/validation.ts2
-rw-r--r--lib/docu-list-rule/combo-box-settings/service.ts34
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-options-add-dialog.tsx22
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-options-detail-sheet.tsx58
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-options-dialog.tsx234
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-options-edit-sheet.tsx52
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-options-expandable-row.tsx263
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-options-table-columns.tsx27
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table-toolbar.tsx33
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table.tsx23
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/delete-combo-box-settings-dialog.tsx85
-rw-r--r--lib/docu-list-rule/combo-box-settings/table/document-class-options-sheet.tsx436
-rw-r--r--lib/docu-list-rule/combo-box-settings/validation.ts38
-rw-r--r--lib/docu-list-rule/document-class/service.ts167
-rw-r--r--lib/docu-list-rule/document-class/table/delete-document-class-dialog.tsx88
-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-edit-sheet.tsx2
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx66
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-option-edit-sheet.tsx84
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-options-detail-sheet.tsx152
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-options-table-columns.tsx15
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-options-table-toolbar.tsx22
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-options-table.tsx176
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-table-columns.tsx8
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-table-toolbar.tsx2
-rw-r--r--lib/docu-list-rule/document-class/table/document-class-table.tsx53
-rw-r--r--lib/docu-list-rule/document-class/validation.ts2
-rw-r--r--lib/docu-list-rule/number-type-configs/repository.ts269
-rw-r--r--lib/docu-list-rule/number-type-configs/service.ts331
-rw-r--r--lib/docu-list-rule/number-type-configs/table/delete-number-type-configs-dialog.tsx2
-rw-r--r--lib/docu-list-rule/number-type-configs/table/number-type-configs-edit-dialog.tsx2
-rw-r--r--lib/docu-list-rule/number-type-configs/table/number-type-configs-table.tsx52
-rw-r--r--lib/docu-list-rule/number-type-configs/table/number-type-configs-toolbar-actions.tsx2
-rw-r--r--lib/docu-list-rule/number-type-configs/validation.ts22
-rw-r--r--lib/docu-list-rule/number-types/service.ts2
-rw-r--r--lib/docu-list-rule/number-types/table/delete-number-types-dialog.tsx4
-rw-r--r--lib/docu-list-rule/number-types/table/number-type-add-dialog.tsx2
-rw-r--r--lib/docu-list-rule/number-types/table/number-type-edit-sheet.tsx2
-rw-r--r--lib/docu-list-rule/number-types/table/number-types-table-toolbar.tsx19
-rw-r--r--lib/docu-list-rule/number-types/table/number-types-table.tsx10
-rw-r--r--lib/docu-list-rule/number-types/validation.ts2
-rw-r--r--lib/docu-list-rule/types.ts3
46 files changed, 913 insertions, 1998 deletions
diff --git a/lib/docu-list-rule/code-groups/service.ts b/lib/docu-list-rule/code-groups/service.ts
index 2c30cedb..c99588a5 100644
--- a/lib/docu-list-rule/code-groups/service.ts
+++ b/lib/docu-list-rule/code-groups/service.ts
@@ -2,7 +2,7 @@
import { revalidatePath } from "next/cache"
import db from "@/db/db"
-import { codeGroups, comboBoxSettings, documentClasses } from "@/db/schema/docu-list-rule"
+import { codeGroups, comboBoxSettings } from "@/db/schema/docu-list-rule"
import { eq, sql, count } from "drizzle-orm"
import { unstable_noStore } from "next/cache"
@@ -64,7 +64,7 @@ export async function getCodeGroups(input: any) {
}
// 정렬 (안전한 필드 체크 적용)
- let orderBy = sql`${codeGroups.createdAt} DESC`
+ let orderBy = sql`${codeGroups.groupId} ASC`
if (input.sort && input.sort.length > 0) {
const sortField = input.sort[0]
// 안전성 체크: 필드가 실제 테이블에 존재하는지 확인
@@ -241,11 +241,6 @@ export async function deleteCodeGroup(id: number) {
.where(eq(comboBoxSettings.codeGroupId, id))
}
- // Document Class가 연결된 경우 Document Class도 삭제
- await db
- .delete(documentClasses)
- .where(eq(documentClasses.codeGroupId, id))
-
// Code Group 삭제
await db
.delete(codeGroups)
@@ -253,7 +248,6 @@ export async function deleteCodeGroup(id: number) {
revalidatePath("/evcp/docu-list-rule/code-groups")
revalidatePath("/evcp/docu-list-rule/combo-box-settings")
- revalidatePath("/evcp/docu-list-rule/document-class")
return {
success: true,
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 660adfed..bf044f1a 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
@@ -19,6 +19,7 @@ import {
import {
Form,
FormControl,
+ FormDescription,
FormField,
FormItem,
FormLabel,
@@ -39,8 +40,8 @@ const createCodeGroupSchema = z.object({
description: z.string().min(1, "Description은 필수입니다."),
codeFormat: z.string().optional().refine((val) => {
if (!val) return true; // 빈 값은 허용
- return /^[AN]*$/.test(val);
- }, "Code Format은 A(영어 대문자) 또는 N(숫자)만 입력 가능합니다."),
+ return /^[ANX]*$/.test(val);
+ }, "Code Format은 A(영어 대문자), N(숫자), X(영어/숫자)만 입력 가능합니다."),
controlType: z.string().min(1, "Control Type은 필수입니다."),
})
@@ -80,6 +81,8 @@ export function CodeGroupsAddDialog({ onSuccess }: CodeGroupsAddDialogProps) {
expression += `[A-Z]{${count}}`
} else if (currentChar === 'N') {
expression += `[0-9]{${count}}`
+ } else if (currentChar === 'X') {
+ expression += `[A-Z0-9]{${count}}`
}
currentChar = codeFormat[i]
count = 1
@@ -91,6 +94,8 @@ export function CodeGroupsAddDialog({ onSuccess }: CodeGroupsAddDialogProps) {
expression += `[A-Z]{${count}}`
} else if (currentChar === 'N') {
expression += `[0-9]{${count}}`
+ } else if (currentChar === 'X') {
+ expression += `[A-Z0-9]{${count}}`
}
expression += '$'
@@ -177,11 +182,14 @@ export function CodeGroupsAddDialog({ onSuccess }: CodeGroupsAddDialogProps) {
<FormLabel>Code Format</FormLabel>
<FormControl>
<Input
- placeholder="예: AANNN (A:영어대문자, N:숫자)"
+ placeholder="예: AANNN (A:영어대문자, N:숫자, X:영어/숫자)"
{...field}
onBlur={() => form.trigger('codeFormat')}
/>
</FormControl>
+ <FormDescription>
+ A: 영어 대문자, N: 숫자, X: 영어 대문자 또는 숫자
+ </FormDescription>
<FormMessage />
</FormItem>
)}
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 28aebd54..74ebc05a 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
@@ -19,6 +19,7 @@ import {
import {
Form,
FormControl,
+ FormDescription,
FormField,
FormItem,
FormLabel,
@@ -41,8 +42,8 @@ const updateCodeGroupSchema = z.object({
description: z.string().min(1, "Description은 필수입니다."),
codeFormat: z.string().optional().refine((val) => {
if (!val) return true; // 빈 값은 허용
- return /^[AN]*$/.test(val);
- }, "Code Format은 A(영어 대문자) 또는 N(숫자)만 입력 가능합니다."),
+ return /^[ANX]*$/.test(val);
+ }, "Code Format은 A(영어 대문자), N(숫자), X(영어/숫자)만 입력 가능합니다."),
controlType: z.string().min(1, "Control Type은 필수입니다."),
isActive: z.boolean().default(true),
})
@@ -92,6 +93,8 @@ export function CodeGroupsEditSheet({
expression += `[A-Z]{${count}}`
} else if (currentChar === 'N') {
expression += `[0-9]{${count}}`
+ } else if (currentChar === 'X') {
+ expression += `[A-Z0-9]{${count}}`
}
currentChar = codeFormat[i]
count = 1
@@ -103,6 +106,8 @@ export function CodeGroupsEditSheet({
expression += `[A-Z]{${count}}`
} else if (currentChar === 'N') {
expression += `[0-9]{${count}}`
+ } else if (currentChar === 'X') {
+ expression += `[A-Z0-9]{${count}}`
}
expression += '$'
@@ -183,11 +188,14 @@ export function CodeGroupsEditSheet({
<FormLabel>Code Format</FormLabel>
<FormControl>
<Input
- placeholder="예: AANNN (A:영어대문자, N:숫자)"
+ placeholder="예: AANNN (A:영어대문자, N:숫자, X:영어/숫자)"
{...field}
onBlur={() => form.trigger('codeFormat')}
/>
</FormControl>
+ <FormDescription>
+ A: 영어 대문자, N: 숫자, X: 영어 대문자 또는 숫자
+ </FormDescription>
<FormMessage />
</FormItem>
)}
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 c10d3445..8873c34c 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
@@ -48,8 +48,6 @@ export function CodeGroupsTable({ promises }: CodeGroupsTableProps) {
id: "controlType", label: "Control Type", type: "select", options: [
{ label: "Textbox", value: "textbox" },
{ label: "Combobox", value: "combobox" },
- { label: "Date", value: "date" },
- { label: "Number", value: "number" },
]
},
{
@@ -69,7 +67,6 @@ export function CodeGroupsTable({ promises }: CodeGroupsTableProps) {
enablePinning: true,
enableAdvancedFilter: true,
initialState: {
- sorting: [{ id: "createdAt", desc: true }],
columnPinning: { right: ["actions"] },
},
getRowId: (originalRow) => String(originalRow.groupId),
diff --git a/lib/docu-list-rule/code-groups/validation.ts b/lib/docu-list-rule/code-groups/validation.ts
index 90e06d0c..9745841c 100644
--- a/lib/docu-list-rule/code-groups/validation.ts
+++ b/lib/docu-list-rule/code-groups/validation.ts
@@ -17,7 +17,7 @@ export const searchParamsCodeGroupsCache = createSearchParamsCache({
page: parseAsInteger.withDefault(1),
perPage: parseAsInteger.withDefault(10),
sort: getSortingStateParser<typeof codeGroups.$inferSelect>().withDefault([
- { id: "createdAt", desc: true },
+ { id: "groupId", desc: false },
]),
groupId: parseAsString.withDefault(""),
diff --git a/lib/docu-list-rule/combo-box-settings/service.ts b/lib/docu-list-rule/combo-box-settings/service.ts
index 2c5ee42b..80c1942d 100644
--- a/lib/docu-list-rule/combo-box-settings/service.ts
+++ b/lib/docu-list-rule/combo-box-settings/service.ts
@@ -74,7 +74,7 @@ export async function getComboBoxCodeGroups(input: {
}
// 정렬 (안전한 필드 체크 적용)
- let orderBy = sql`${codeGroups.createdAt} DESC`
+ let orderBy = sql`${codeGroups.groupId} ASC`
if (sort && sort.length > 0) {
const sortField = sort[0]
// 안전성 체크: 필드가 실제 테이블에 존재하는지 확인
@@ -155,7 +155,7 @@ export async function getComboBoxOptions(codeGroupId: number, input?: {
}
// 정렬 (안전한 필드 체크 적용)
- let orderBy = sql`${comboBoxSettings.createdAt} DESC`
+ let orderBy = sql`${comboBoxSettings.code} ASC`
if (sort && sort.length > 0) {
const sortField = sort[0]
// 안전성 체크: 필드가 실제 테이블에 존재하는지 확인
@@ -281,6 +281,36 @@ export async function updateComboBoxOption(input: {
remark?: string
}) {
try {
+ // 현재 수정 중인 항목의 codeGroupId 가져오기
+ const currentOption = await db
+ .select({ codeGroupId: comboBoxSettings.codeGroupId })
+ .from(comboBoxSettings)
+ .where(eq(comboBoxSettings.id, input.id))
+ .limit(1)
+
+ if (currentOption.length === 0) {
+ return {
+ success: false,
+ error: "Option not found"
+ }
+ }
+
+ // 코드 중복 체크 (현재 수정 중인 항목 제외)
+ const existingOption = await db
+ .select({ id: comboBoxSettings.id })
+ .from(comboBoxSettings)
+ .where(
+ sql`${comboBoxSettings.codeGroupId} = ${currentOption[0].codeGroupId} AND ${comboBoxSettings.code} = ${input.code} AND ${comboBoxSettings.id} != ${input.id}`
+ )
+ .limit(1)
+
+ if (existingOption.length > 0) {
+ return {
+ success: false,
+ error: "이미 존재하는 코드입니다."
+ }
+ }
+
const [updatedOption] = await db
.update(comboBoxSettings)
.set({
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 a5a8af2f..049e2c1a 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
@@ -31,7 +31,7 @@ import { createComboBoxOption } from "../service"
const createOptionSchema = z.object({
code: z.string().min(1, "코드는 필수입니다."),
- description: z.string().default("-"),
+ description: z.string().default(""),
remark: z.string().optional(),
})
@@ -50,18 +50,24 @@ export function ComboBoxOptionsAddDialog({ codeGroupId, onSuccess }: ComboBoxOpt
resolver: zodResolver(createOptionSchema),
defaultValues: {
code: "",
- description: "-",
+ description: "",
remark: "",
},
})
+ // 코드 입력 시 자동으로 description에 반영
+ const handleCodeChange = (value: string) => {
+ form.setValue("code", value)
+ form.setValue("description", value) // 코드값을 description에도 자동 설정
+ }
+
const handleSubmit = (data: CreateOptionSchema) => {
startTransition(async () => {
try {
const result = await createComboBoxOption({
codeGroupId,
code: data.code,
- description: data.description || "-",
+ description: data.description || data.code, // description이 비어있으면 code 사용
remark: data.remark,
})
@@ -109,7 +115,11 @@ export function ComboBoxOptionsAddDialog({ codeGroupId, onSuccess }: ComboBoxOpt
<FormItem>
<FormLabel>코드</FormLabel>
<FormControl>
- <Input {...field} placeholder="옵션 코드" />
+ <Input
+ {...field}
+ placeholder="옵션 코드"
+ onChange={(e) => handleCodeChange(e.target.value)}
+ />
</FormControl>
<FormMessage />
</FormItem>
@@ -120,9 +130,9 @@ export function ComboBoxOptionsAddDialog({ codeGroupId, onSuccess }: ComboBoxOpt
name="description"
render={({ field }) => (
<FormItem>
- <FormLabel>값</FormLabel>
+ <FormLabel>Description</FormLabel>
<FormControl>
- <Input {...field} placeholder="옵션 값" />
+ <Input {...field} placeholder="옵션 설명" />
</FormControl>
<FormMessage />
</FormItem>
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 0b2a76a4..b62b258e 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
@@ -1,22 +1,20 @@
"use client"
import * as React from "react"
-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 { useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel } from "@tanstack/react-table"
+import { DataTableDetail } from "@/components/data-table/data-table-detail"
+import { DataTableAdvancedToolbarDetail } from "@/components/data-table/data-table-advanced-toolbar-detail"
import type { DataTableAdvancedFilterField, DataTableRowAction } from "@/types/table"
import {
Sheet,
SheetContent,
} from "@/components/ui/sheet"
-
-import { getComboBoxOptions } from "../service"
-import { getColumns } from "./combo-box-options-table-columns"
-import { ComboBoxOptionsEditSheet } from "./combo-box-options-edit-sheet"
-import { DeleteComboBoxOptionsDialog } from "./delete-combo-box-options-dialog"
-import { ComboBoxOptionsTableToolbarActions } from "./combo-box-options-table-toolbar"
-import { codeGroups } from "@/db/schema/codeGroups"
-
+import { getComboBoxOptions } from "@/lib/docu-list-rule/combo-box-settings/service"
+import { getColumns } from "@/lib/docu-list-rule/combo-box-settings/table/combo-box-options-table-columns"
+import { ComboBoxOptionsEditSheet } from "@/lib/docu-list-rule/combo-box-settings/table/combo-box-options-edit-sheet"
+import { DeleteComboBoxOptionsDialog } from "@/lib/docu-list-rule/combo-box-settings/table/delete-combo-box-options-dialog"
+import { ComboBoxOptionsTableToolbarActions } from "@/lib/docu-list-rule/combo-box-settings/table/combo-box-options-table-toolbar"
+import { codeGroups } from "@/db/schema"
type ComboBoxOption = {
id: number
codeGroupId: number
@@ -88,10 +86,6 @@ export function ComboBoxOptionsDetailSheet({
const result = await getComboBoxOptions(codeGroup.id, {
page: 1,
perPage: 10,
- search: table.getState().globalFilter || "",
- sort: table.getState().sorting,
- filters: table.getState().columnFilters,
- joinOperator: "and",
})
if (result.success && result.data) {
const optionsWithIsActive = result.data.map(option => ({
@@ -112,27 +106,25 @@ export function ComboBoxOptionsDetailSheet({
// 고급 필터 필드 설정
const advancedFilterFields: DataTableAdvancedFilterField<any>[] = [
- { id: "code", label: "코드", type: "text" },
- { id: "description", label: "값", type: "text" },
- { id: "remark", label: "비고", type: "text" },
+ { id: "code", label: "code", type: "text" },
+ { id: "remark", label: "remark", type: "text" },
+ { id: "updatedAt", label: "updated_at", type: "date" },
]
- const { table } = useDataTable({
+ const table = useReactTable({
data: rawData.data as any,
columns: columns as any,
- pageCount: rawData.pageCount,
- enablePinning: true,
- enableAdvancedFilter: true,
- manualSorting: true,
- manualFiltering: true,
- manualPagination: true, // 수동 페이징 활성화
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
initialState: {
- sorting: [{ id: "createdAt", desc: true }],
- columnPinning: { right: ["actions"] },
+ sorting: [{ id: "code", desc: false }],
+ pagination: {
+ pageSize: 10,
+ },
},
getRowId: (originalRow) => String((originalRow as any).id),
- shallow: false,
- clearOnDefault: true,
})
if (!codeGroup) return null
@@ -144,7 +136,7 @@ export function ComboBoxOptionsDetailSheet({
<div>
<h3 className="text-lg font-medium">{codeGroup.description} 옵션 관리</h3>
<p className="text-sm text-muted-foreground">
- {codeGroup.groupId}의 Combo Box 옵션들을 관리합니다.
+ {codeGroup.description}의 Combo Box 옵션들을 관리합니다.
</p>
</div>
</div>
@@ -155,12 +147,12 @@ export function ComboBoxOptionsDetailSheet({
onSuccess={refreshData}
/>
- <DataTable table={table as any}>
- <DataTableAdvancedToolbar
+ <DataTableDetail table={table as any}>
+ <DataTableAdvancedToolbarDetail
table={table as any}
filterFields={advancedFilterFields}
/>
- </DataTable>
+ </DataTableDetail>
<DeleteComboBoxOptionsDialog
open={rowAction?.type === "delete"}
diff --git a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-dialog.tsx b/lib/docu-list-rule/combo-box-settings/table/combo-box-options-dialog.tsx
deleted file mode 100644
index 6459ae14..00000000
--- a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-dialog.tsx
+++ /dev/null
@@ -1,234 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useState } from "react"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Plus, Trash2 } from "lucide-react"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table"
-import { toast } from "sonner"
-import { codeGroups } from "@/db/schema/codeGroups"
-import { createComboBoxOption } from "../service"
-
-interface ComboBoxOptionsDialogProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- codeGroup: typeof codeGroups.$inferSelect | null
- onSuccess: () => void
-}
-
-interface OptionRow {
- id: string
- description: string
- remark: string
-}
-
-export function ComboBoxOptionsDialog({
- open,
- onOpenChange,
- codeGroup,
- onSuccess
-}: ComboBoxOptionsDialogProps) {
- const [optionRows, setOptionRows] = useState<OptionRow[]>([])
- const [loading, setLoading] = useState(false)
-
- // 다이얼로그가 열릴 때 초기 행 생성
- React.useEffect(() => {
- if (open && optionRows.length === 0) {
- addRow()
- }
- }, [open])
-
- // 새 행 추가
- const addRow = () => {
- const newRow: OptionRow = {
- id: `row-${Date.now()}-${Math.random()}`,
- description: "",
- remark: "",
- }
- setOptionRows(prev => [...prev, newRow])
- }
-
- // 행 삭제
- const removeRow = (id: string) => {
- setOptionRows(prev => prev.filter(row => row.id !== id))
- }
-
- // 행 업데이트
- const updateRow = (id: string, field: keyof OptionRow, value: string) => {
- setOptionRows(prev =>
- prev.map(row =>
- row.id === id ? { ...row, [field]: value } : row
- )
- )
- }
-
- // 일괄 저장
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
-
- if (!codeGroup) return
-
- // 유효한 행들만 필터링 (Description이 있는 것만)
- const validRows = optionRows.filter(row => row.description.trim())
-
- if (validRows.length === 0) {
- toast.error("최소 하나의 Description을 입력해주세요.")
- return
- }
-
- setLoading(true)
- try {
- let successCount = 0
- let errorCount = 0
-
- // 각 행을 순차적으로 저장
- for (const row of validRows) {
- try {
- const result = await createComboBoxOption({
- codeGroupId: codeGroup.id,
- code: "", // 서비스에서 자동 생성
- description: row.description.trim(),
- remark: row.remark.trim() || undefined,
- })
-
- if (result.success) {
- successCount++
- } else {
- errorCount++
- }
- } catch (error) {
- console.error("옵션 추가 실패:", error)
- errorCount++
- }
- }
-
- if (successCount > 0) {
- toast.success(`${successCount}개의 옵션이 추가되었습니다.${errorCount > 0 ? ` (${errorCount}개 실패)` : ''}`)
- // 폼 초기화
- setOptionRows([])
- onSuccess()
- } else {
- toast.error("모든 옵션 추가에 실패했습니다.")
- }
- } catch (error) {
- console.error("옵션 추가 실패:", error)
- toast.error("옵션 추가에 실패했습니다.")
- } finally {
- setLoading(false)
- }
- }
-
- const handleCancel = () => {
- // 폼 초기화
- setOptionRows([])
- onOpenChange(false)
- }
-
- return (
- <Dialog open={open} onOpenChange={onOpenChange}>
- <DialogContent className="sm:max-w-[600px] max-h-[80vh] overflow-y-auto">
- <DialogHeader>
- <DialogTitle>Combo Box 옵션 추가</DialogTitle>
- <DialogDescription>
- {codeGroup?.description}에 새로운 옵션들을 추가합니다. Code는 자동으로 생성됩니다.
- </DialogDescription>
- </DialogHeader>
-
- <div className="space-y-4">
- {/* 테이블 헤더와 추가 버튼 */}
- <div className="flex items-center justify-between">
- <h4 className="text-sm font-medium">옵션 목록</h4>
- <Button
- type="button"
- variant="outline"
- size="sm"
- onClick={addRow}
- className="h-8"
- >
- <Plus className="h-4 w-4 mr-1" />
- 행 추가
- </Button>
- </div>
-
- {/* 옵션 테이블 - 항상 표시 */}
- <div className="border rounded-lg overflow-hidden">
- <Table>
- <TableHeader>
- <TableRow className="bg-muted/30">
- <TableHead className="w-[50%]">Description *</TableHead>
- <TableHead className="w-[40%]">Remark</TableHead>
- <TableHead className="w-[10%]"></TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {optionRows.map((row) => (
- <TableRow key={row.id} className="hover:bg-muted/30">
- <TableCell>
- <Input
- value={row.description}
- onChange={(e) => updateRow(row.id, "description", e.target.value)}
- className="border-0 focus-visible:ring-1 bg-transparent h-8"
- />
- </TableCell>
- <TableCell>
- <Input
- value={row.remark}
- onChange={(e) => updateRow(row.id, "remark", e.target.value)}
- className="border-0 focus-visible:ring-1 bg-transparent h-8"
- />
- </TableCell>
- <TableCell>
- <Button
- type="button"
- variant="ghost"
- size="sm"
- onClick={() => removeRow(row.id)}
- className="h-6 w-6 p-0"
- >
- <Trash2 className="h-3 w-3" />
- </Button>
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </div>
- </div>
-
- <DialogFooter>
- <Button
- type="button"
- variant="outline"
- onClick={handleCancel}
- disabled={loading}
- >
- 취소
- </Button>
- <Button
- type="submit"
- onClick={handleSubmit}
- disabled={loading}
- >
- {loading ? "저장 중..." : "저장"}
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- )
-} \ No newline at end of file
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 5732674e..4ac539d0 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
@@ -5,10 +5,17 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
-import { Loader } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet"
+import {
Form,
FormControl,
FormField,
@@ -17,19 +24,12 @@ import {
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet"
import { updateComboBoxOption } from "../service"
const updateOptionSchema = z.object({
- value: z.string().min(1, "값은 필수입니다."),
+ code: z.string().min(1, "코드는 필수입니다."),
+ remark: z.string().optional(),
})
type UpdateOptionSchema = z.infer<typeof updateOptionSchema>
@@ -63,14 +63,16 @@ export function ComboBoxOptionsEditSheet({
const form = useForm<UpdateOptionSchema>({
resolver: zodResolver(updateOptionSchema),
defaultValues: {
- value: "",
+ code: "",
+ remark: "",
},
})
React.useEffect(() => {
if (data) {
form.reset({
- value: data.description,
+ code: data.code,
+ remark: data.remark || "",
})
}
}, [data, form])
@@ -82,7 +84,9 @@ export function ComboBoxOptionsEditSheet({
try {
const result = await updateComboBoxOption({
id: data.id,
- value: formData.value,
+ code: formData.code,
+ description: data.description, // 기존 description 유지
+ remark: formData.remark,
})
if (result.success) {
@@ -117,12 +121,25 @@ export function ComboBoxOptionsEditSheet({
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<FormField
control={form.control}
- name="value"
+ name="code"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>code</FormLabel>
+ <FormControl>
+ <Input {...field} placeholder="옵션 코드" />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="remark"
render={({ field }) => (
<FormItem>
- <FormLabel>값</FormLabel>
+ <FormLabel>remark</FormLabel>
<FormControl>
- <Input {...field} placeholder="옵션 값" />
+ <Input {...field} placeholder="비고 (선택사항)" />
</FormControl>
<FormMessage />
</FormItem>
@@ -133,9 +150,6 @@ export function ComboBoxOptionsEditSheet({
취소
</Button>
<Button type="submit" disabled={isPending || !form.formState.isValid}>
- {isPending && (
- <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
- )}
수정
</Button>
</SheetFooter>
diff --git a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-expandable-row.tsx b/lib/docu-list-rule/combo-box-settings/table/combo-box-options-expandable-row.tsx
deleted file mode 100644
index 07b63de5..00000000
--- a/lib/docu-list-rule/combo-box-settings/table/combo-box-options-expandable-row.tsx
+++ /dev/null
@@ -1,263 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useState, useEffect } from "react"
-import { MoreHorizontal, Settings } from "lucide-react"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table"
-import { toast } from "sonner"
-import { codeGroups } from "@/db/schema/codeGroups"
-import { getComboBoxOptions, updateComboBoxOption, deleteComboBoxOption } from "../service"
-import { DocumentClassOptionsSheet } from "./document-class-options-sheet"
-
-
-interface ComboBoxOptionsExpandableRowProps {
- codeGroup: typeof codeGroups.$inferSelect
-}
-
-interface ComboBoxOption {
- id: number
- codeGroupId: number
- code: string
- description: string
- remark: string | null
- createdAt: Date
- updatedAt: Date
-}
-
-export function ComboBoxOptionsExpandableRow({ codeGroup }: ComboBoxOptionsExpandableRowProps) {
- const [options, setOptions] = useState<ComboBoxOption[]>([])
- const [loading, setLoading] = useState(true)
- const [editingOption, setEditingOption] = useState<ComboBoxOption | null>(null)
- const [selectedOptionForSubOptions, setSelectedOptionForSubOptions] = useState<ComboBoxOption | null>(null)
-
- // 옵션 목록 로드
- const loadOptions = async () => {
- try {
- setLoading(true)
- const result = await getComboBoxOptions(codeGroup.id)
- if (result.success && result.data) {
- setOptions(result.data as ComboBoxOption[])
- } else {
- toast.error("옵션 목록을 불러오는데 실패했습니다.")
- }
- } catch (error) {
- console.error("옵션 로드 실패:", error)
- toast.error("옵션 목록을 불러오는데 실패했습니다.")
- } finally {
- setLoading(false)
- }
- }
-
- useEffect(() => {
- loadOptions()
- }, [codeGroup.id])
-
- // 기존 옵션 수정
- const handleUpdateOption = async (option: ComboBoxOption) => {
- if (!option.code.trim() || !option.description.trim()) {
- toast.error("Code와 Description은 필수 입력 항목입니다.")
- return
- }
-
- try {
- const result = await updateComboBoxOption({
- id: option.id,
- code: option.code.trim(),
- description: option.description.trim(),
- remark: option.remark || undefined,
- })
-
- if (result.success) {
- await loadOptions() // 목록 새로고침
- setEditingOption(null) // 편집 모드 종료
- toast.success("옵션이 수정되었습니다.")
- } else {
- toast.error("옵션 수정에 실패했습니다.")
- }
- } catch (error) {
- console.error("옵션 수정 실패:", error)
- toast.error("옵션 수정에 실패했습니다.")
- }
- }
-
- // 기존 옵션 삭제
- const handleDeleteOption = async (optionId: number) => {
- if (!confirm("정말로 이 옵션을 삭제하시겠습니까?")) {
- return
- }
-
- try {
- const result = await deleteComboBoxOption(optionId)
- if (result.success) {
- await loadOptions() // 목록 새로고침
- toast.success("옵션이 삭제되었습니다.")
- } else {
- toast.error("옵션 삭제에 실패했습니다.")
- }
- } catch (error) {
- console.error("옵션 삭제 실패:", error)
- toast.error("옵션 삭제에 실패했습니다.")
- }
- }
-
- // Document Class인지 확인 (Description이 "Document Class"인 경우)
- const isDocumentClass = codeGroup.description === "Document Class"
-
- return (
- <div className="bg-muted/20 border-t">
- <div className="space-y-0 ml-[60px]">
- {/* 커스텀 테이블 */}
- <div className="border overflow-hidden bg-white">
- <Table className="w-full table-fixed">
- <TableHeader>
- <TableRow className="bg-muted/30">
- <TableHead className="w-[20%] font-medium text-muted-foreground">Code</TableHead>
- <TableHead className="w-[30%] font-medium text-muted-foreground">Description</TableHead>
- <TableHead className="w-[25%] font-medium text-muted-foreground">Remark</TableHead>
- {isDocumentClass && (
- <TableHead className="w-[15%] font-medium text-muted-foreground">하위 옵션</TableHead>
- )}
- <TableHead className="w-[10%] font-medium text-muted-foreground">작업</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {/* 기존 옵션들 */}
- {options.map((option) => (
- <TableRow key={option.id} className="hover:bg-muted/30 transition-colors">
- <TableCell className="font-medium text-sm">
- {editingOption?.id === option.id ? (
- <Input
- value={editingOption.code}
- onChange={(e) => setEditingOption(prev => prev ? { ...prev, code: e.target.value } : null)}
- placeholder="Code (*)"
- className="border-0 focus-visible:ring-1 bg-transparent h-8"
- />
- ) : (
- option.code
- )}
- </TableCell>
- <TableCell className="text-sm">
- {editingOption?.id === option.id ? (
- <Input
- value={editingOption.description}
- onChange={(e) => setEditingOption(prev => prev ? { ...prev, description: e.target.value } : null)}
- placeholder="Description (*)"
- className="border-0 focus-visible:ring-1 bg-transparent h-8"
- />
- ) : (
- option.description
- )}
- </TableCell>
- <TableCell className="text-sm text-muted-foreground">
- {editingOption?.id === option.id ? (
- <Input
- value={editingOption.remark || ""}
- onChange={(e) => setEditingOption(prev => prev ? { ...prev, remark: e.target.value } : null)}
- placeholder="Remark"
- className="border-0 focus-visible:ring-1 bg-transparent h-8"
- />
- ) : (
- option.remark || "-"
- )}
- </TableCell>
- {isDocumentClass && (
- <TableCell className="text-sm">
- <Button
- variant="outline"
- size="sm"
- onClick={() => setSelectedOptionForSubOptions(option)}
- className="h-6 px-2 text-xs"
- >
- <Settings className="h-3 w-3 mr-1" />
- 관리
- </Button>
- </TableCell>
- )}
- <TableCell className="text-sm">
- {editingOption?.id === option.id ? (
- <div className="flex gap-1">
- <Button
- onClick={() => handleUpdateOption(editingOption)}
- size="sm"
- variant="outline"
- className="h-6 px-2 text-xs"
- >
- 저장
- </Button>
- <Button
- onClick={() => setEditingOption(null)}
- size="sm"
- variant="ghost"
- className="h-6 px-2 text-xs"
- >
- 취소
- </Button>
- </div>
- ) : (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- variant="ghost"
- className="h-6 w-6 p-0"
- >
- <MoreHorizontal className="h-3 w-3" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end">
- <DropdownMenuItem onClick={() => setEditingOption(option)}>
- 수정
- </DropdownMenuItem>
- <DropdownMenuSeparator />
- <DropdownMenuItem onClick={() => handleDeleteOption(option.id)}>
- 삭제
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- )}
- </TableCell>
- </TableRow>
- ))}
-
- {options.length === 0 && (
- <TableRow>
- <TableCell colSpan={isDocumentClass ? 5 : 4} className="text-center text-muted-foreground py-8">
- 등록된 옵션이 없습니다.
- </TableCell>
- </TableRow>
- )}
- </TableBody>
- </Table>
- </div>
- </div>
-
- {/* Document Class 하위 옵션 관리 시트 */}
- {selectedOptionForSubOptions && (
- <DocumentClassOptionsSheet
- open={!!selectedOptionForSubOptions}
- onOpenChange={(open) => !open && setSelectedOptionForSubOptions(null)}
- comboBoxOption={selectedOptionForSubOptions}
- onSuccess={() => {
- setSelectedOptionForSubOptions(null)
- // 필요시 하위 옵션 목록 새로고침
- }}
- />
- )}
- </div>
- )
-} \ No newline at end of file
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 e5780e9e..0e46c0ed 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
@@ -114,40 +114,43 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<ComboBo
accessorKey: "code",
enableResizing: true,
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="코드" />
+ <DataTableColumnHeaderSimple column={column} title="code" />
),
meta: {
- excelHeader: "코드",
+ excelHeader: "code",
type: "text",
},
cell: ({ row }) => row.getValue("code") ?? "",
minSize: 80
},
{
- accessorKey: "description",
+ accessorKey: "remark",
enableResizing: true,
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="값" />
+ <DataTableColumnHeaderSimple column={column} title="remark" />
),
meta: {
- excelHeader: "값",
+ excelHeader: "remark",
type: "text",
},
- cell: ({ row }) => row.getValue("description") ?? "",
+ cell: ({ row }) => row.getValue("remark") ?? "",
minSize: 80
},
{
- accessorKey: "remark",
+ accessorKey: "updatedAt",
enableResizing: true,
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="비고" />
+ <DataTableColumnHeaderSimple column={column} title="updated_at" />
),
meta: {
- excelHeader: "비고",
- type: "text",
+ excelHeader: "updated_at",
+ type: "date",
},
- cell: ({ row }) => row.getValue("remark") ?? "",
- minSize: 80
+ cell: ({ row }) => {
+ const date = row.getValue("updatedAt") as Date
+ return date ? new Date(date).toLocaleDateString('ko-KR') : ""
+ },
+ minSize: 100
}
]
diff --git a/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table-toolbar.tsx b/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table-toolbar.tsx
deleted file mode 100644
index 77cbea01..00000000
--- a/lib/docu-list-rule/combo-box-settings/table/combo-box-settings-table-toolbar.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type Table } from "@tanstack/react-table"
-
-import { DeleteComboBoxSettingsDialog } from "./delete-combo-box-settings-dialog"
-import { codeGroups } from "@/db/schema/codeGroups"
-
-interface ComboBoxSettingsTableToolbarActionsProps {
- table: Table<typeof codeGroups.$inferSelect>
- onSuccess?: () => void
-}
-
-export function ComboBoxSettingsTableToolbarActions({ table, onSuccess }: ComboBoxSettingsTableToolbarActionsProps) {
- return (
- <div className="flex items-center gap-2">
- {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */}
- {table.getFilteredSelectedRowModel().rows.length > 0 ? (
- <DeleteComboBoxSettingsDialog
- codeGroups={table
- .getFilteredSelectedRowModel()
- .rows.map((row) => row.original)}
- onSuccess={() => {
- table.toggleAllRowsSelected(false)
- onSuccess?.()
- }}
- />
- ) : null}
-
-
- </div>
- )
-} \ No newline at end of file
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 f6216363..42ce1a19 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,15 +1,12 @@
"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"
import type { DataTableAdvancedFilterField } from "@/types/table"
-import { getComboBoxCodeGroups } from "../service"
import { getColumns } from "./combo-box-settings-table-columns"
import { ComboBoxOptionsDetailSheet } from "./combo-box-options-detail-sheet"
-import { ComboBoxSettingsTableToolbarActions } from "./combo-box-settings-table-toolbar"
import { codeGroups } from "@/db/schema/docu-list-rule"
interface ComboBoxSettingsTableProps {
@@ -17,21 +14,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(() => {
- // 전체 페이지 새로고침 대신 router.refresh() 사용 (성능 개선)
- router.refresh()
- }, [router])
-
// Detail 버튼 클릭 핸들러
- const handleDetail = (codeGroup: typeof codeGroups.$inferSelect) => {
+ const handleDetail = React.useCallback((codeGroup: typeof codeGroups.$inferSelect) => {
setSelectedCodeGroup(codeGroup)
setIsDetailSheetOpen(true)
- }
+ }, [])
const columns = React.useMemo(() => getColumns({ onDetail: handleDetail }), [handleDetail])
@@ -44,8 +35,6 @@ export function ComboBoxSettingsTable({ promises }: ComboBoxSettingsTableProps)
id: "controlType", label: "Control Type", type: "select", options: [
{ label: "Textbox", value: "textbox" },
{ label: "Combobox", value: "combobox" },
- { label: "Date", value: "date" },
- { label: "Number", value: "number" },
]
},
{
@@ -58,14 +47,12 @@ export function ComboBoxSettingsTable({ promises }: ComboBoxSettingsTableProps)
]
const { table } = useDataTable({
- data: rawData[0].data as any,
+ data: rawData[0].data as typeof codeGroups.$inferSelect[],
columns,
pageCount: rawData[0].pageCount,
enablePinning: true,
enableAdvancedFilter: true,
- manualSorting: false,
initialState: {
- sorting: [{ id: "createdAt", desc: true }],
columnPinning: { right: ["actions"] },
columnFilters: [
{
@@ -86,7 +73,7 @@ export function ComboBoxSettingsTable({ promises }: ComboBoxSettingsTableProps)
table={table}
filterFields={advancedFilterFields}
>
- <ComboBoxSettingsTableToolbarActions table={table} onSuccess={refreshData} />
+
</DataTableAdvancedToolbar>
</DataTable>
diff --git a/lib/docu-list-rule/combo-box-settings/table/delete-combo-box-settings-dialog.tsx b/lib/docu-list-rule/combo-box-settings/table/delete-combo-box-settings-dialog.tsx
deleted file mode 100644
index 28788bd7..00000000
--- a/lib/docu-list-rule/combo-box-settings/table/delete-combo-box-settings-dialog.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useRouter } from "next/navigation"
-import { AlertTriangle } from "lucide-react"
-
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-import { deleteCodeGroup } from "@/lib/docu-list-rule/code-groups/service"
-import { codeGroups } from "@/db/schema/codeGroups"
-
-interface DeleteComboBoxSettingsDialogProps {
- codeGroups: typeof codeGroups.$inferSelect[]
- onSuccess?: () => void
-}
-
-export function DeleteComboBoxSettingsDialog({
- codeGroups,
- onSuccess,
-}: DeleteComboBoxSettingsDialogProps) {
- const router = useRouter()
- const [isDeleting, setIsDeleting] = React.useState(false)
-
- const handleDelete = React.useCallback(async () => {
- if (codeGroups.length === 0) return
-
- setIsDeleting(true)
- try {
- for (const codeGroup of codeGroups) {
- await deleteCodeGroup(codeGroup.id)
- }
-
- router.refresh()
- onSuccess?.()
- } catch (error) {
- console.error("Error deleting code groups:", error)
- } finally {
- setIsDeleting(false)
- }
- }, [codeGroups, router, onSuccess])
-
- if (codeGroups.length === 0) {
- return null
- }
-
- return (
- <Dialog>
- <DialogContent>
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2">
- <AlertTriangle className="h-5 w-5 text-destructive" />
- Code Group 삭제
- </DialogTitle>
- <DialogDescription>
- 선택한 Code Group{codeGroups.length > 1 ? "들" : ""}을 삭제하시겠습니까?
- <br />
- 이 작업은 되돌릴 수 없습니다.
- </DialogDescription>
- </DialogHeader>
- <DialogFooter>
- <Button
- variant="outline"
- disabled={isDeleting}
- >
- 취소
- </Button>
- <Button
- variant="destructive"
- onClick={handleDelete}
- disabled={isDeleting}
- >
- {isDeleting ? "삭제 중..." : "삭제"}
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- )
-} \ No newline at end of file
diff --git a/lib/docu-list-rule/combo-box-settings/table/document-class-options-sheet.tsx b/lib/docu-list-rule/combo-box-settings/table/document-class-options-sheet.tsx
deleted file mode 100644
index 8585d9a3..00000000
--- a/lib/docu-list-rule/combo-box-settings/table/document-class-options-sheet.tsx
+++ /dev/null
@@ -1,436 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useState, useEffect } from "react"
-import { Plus, Trash2, Save, X } from "lucide-react"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet"
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table"
-import { toast } from "sonner"
-import {
- getDocumentClassOptions,
- createDocumentClassOption,
- updateDocumentClassOption,
- deleteDocumentClassOption
-} from "../service"
-
-interface DocumentClassOptionsSheetProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- comboBoxOption: {
- id: number
- code: string
- description: string
- }
- onSuccess: () => void
-}
-
-interface DocumentClassOption {
- id: number
- comboBoxSettingId: number
- optionValue: string
- optionCode: string | null
- sortOrder: number
- isActive: boolean
- createdAt: Date
- updatedAt: Date
-}
-
-interface NewOptionRow {
- id: string
- optionValue: string
- optionCode: string
- sortOrder: number
-}
-
-export function DocumentClassOptionsSheet({
- open,
- onOpenChange,
- comboBoxOption,
- onSuccess
-}: DocumentClassOptionsSheetProps) {
- const [options, setOptions] = useState<DocumentClassOption[]>([])
- const [loading, setLoading] = useState(true)
- const [newOptionRows, setNewOptionRows] = useState<NewOptionRow[]>([])
- const [editingOption, setEditingOption] = useState<DocumentClassOption | null>(null)
-
- // 하위 옵션 목록 로드
- const loadOptions = async () => {
- try {
- setLoading(true)
- const result = await getDocumentClassOptions(comboBoxOption.id)
- if (result.success && result.data) {
- setOptions(result.data)
- } else {
- toast.error("하위 옵션 목록을 불러오는데 실패했습니다.")
- }
- } catch (error) {
- console.error("하위 옵션 로드 실패:", error)
- toast.error("하위 옵션 목록을 불러오는데 실패했습니다.")
- } finally {
- setLoading(false)
- }
- }
-
- useEffect(() => {
- if (open) {
- loadOptions()
- }
- }, [open, comboBoxOption.id])
-
- // 새 행 추가
- const addNewRow = () => {
- const newRow: NewOptionRow = {
- id: `new-${Date.now()}-${Math.random()}`,
- optionValue: "",
- optionCode: "",
- sortOrder: options.length + newOptionRows.length + 1,
- }
- setNewOptionRows(prev => [...prev, newRow])
- }
-
- // 새 행 삭제
- const removeNewRow = (id: string) => {
- setNewOptionRows(prev => prev.filter(row => row.id !== id))
- }
-
- // 새 행 업데이트
- const updateNewRow = (id: string, field: keyof NewOptionRow, value: string | number) => {
- setNewOptionRows(prev =>
- prev.map(row =>
- row.id === id ? { ...row, [field]: value } : row
- )
- )
- }
-
- // 새 하위 옵션 저장
- const handleSaveNewOptions = async () => {
- const validRows = newOptionRows.filter(row => row.optionValue.trim())
-
- if (validRows.length === 0) {
- toast.error("최소 하나의 옵션 값을 입력해주세요.")
- return
- }
-
- try {
- let successCount = 0
- let errorCount = 0
-
- for (const row of validRows) {
- try {
- const result = await createDocumentClassOption({
- comboBoxSettingId: comboBoxOption.id,
- optionValue: row.optionValue.trim(),
- optionCode: row.optionCode.trim() || undefined,
- sortOrder: row.sortOrder,
- })
-
- if (result.success) {
- successCount++
- } else {
- errorCount++
- }
- } catch (error) {
- console.error("하위 옵션 추가 실패:", error)
- errorCount++
- }
- }
-
- if (successCount > 0) {
- toast.success(`${successCount}개의 하위 옵션이 추가되었습니다.${errorCount > 0 ? ` (${errorCount}개 실패)` : ''}`)
- setNewOptionRows([])
- await loadOptions()
- onSuccess()
- } else {
- toast.error("모든 하위 옵션 추가에 실패했습니다.")
- }
- } catch (error) {
- console.error("하위 옵션 추가 실패:", error)
- toast.error("하위 옵션 추가에 실패했습니다.")
- }
- }
-
- // 기존 하위 옵션 수정
- const handleUpdateOption = async (option: DocumentClassOption) => {
- if (!option.optionValue.trim()) {
- toast.error("옵션 값은 필수 입력 항목입니다.")
- return
- }
-
- try {
- const result = await updateDocumentClassOption({
- id: option.id,
- optionValue: option.optionValue.trim(),
- optionCode: option.optionCode || undefined,
- sortOrder: option.sortOrder,
- isActive: option.isActive,
- })
-
- if (result.success) {
- await loadOptions()
- setEditingOption(null)
- toast.success("하위 옵션이 수정되었습니다.")
- } else {
- toast.error("하위 옵션 수정에 실패했습니다.")
- }
- } catch (error) {
- console.error("하위 옵션 수정 실패:", error)
- toast.error("하위 옵션 수정에 실패했습니다.")
- }
- }
-
- // 하위 옵션 삭제
- const handleDeleteOption = async (optionId: number) => {
- if (!confirm("정말로 이 하위 옵션을 삭제하시겠습니까?")) {
- return
- }
-
- try {
- const result = await deleteDocumentClassOption(optionId)
- if (result.success) {
- await loadOptions()
- toast.success("하위 옵션이 삭제되었습니다.")
- } else {
- toast.error("하위 옵션 삭제에 실패했습니다.")
- }
- } catch (error) {
- console.error("하위 옵션 삭제 실패:", error)
- toast.error("하위 옵션 삭제에 실패했습니다.")
- }
- }
-
- return (
- <Sheet open={open} onOpenChange={onOpenChange}>
- <SheetContent className="w-[600px] sm:max-w-[600px] overflow-y-auto">
- <SheetHeader>
- <SheetTitle>하위 옵션 관리</SheetTitle>
- <SheetDescription>
- {comboBoxOption.description} ({comboBoxOption.code})의 하위 옵션을 관리합니다.
- </SheetDescription>
- </SheetHeader>
-
- <div className="space-y-4 mt-6">
- {/* 새 하위 옵션 추가 */}
- <div className="space-y-2">
- <div className="flex items-center justify-between">
- <h4 className="text-sm font-medium">새 하위 옵션 추가</h4>
- <Button
- type="button"
- variant="outline"
- size="sm"
- onClick={addNewRow}
- className="h-8"
- >
- <Plus className="h-4 w-4 mr-1" />
- 옵션 추가
- </Button>
- </div>
-
- {newOptionRows.length > 0 && (
- <div className="border rounded-lg overflow-hidden">
- <Table>
- <TableHeader>
- <TableRow className="bg-muted/30">
- <TableHead className="w-[40%]">옵션 값 *</TableHead>
- <TableHead className="w-[30%]">옵션 코드</TableHead>
- <TableHead className="w-[20%]">순서</TableHead>
- <TableHead className="w-[10%]"></TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {newOptionRows.map((row) => (
- <TableRow key={row.id} className="hover:bg-muted/30">
- <TableCell>
- <Input
- value={row.optionValue}
- onChange={(e) => updateNewRow(row.id, "optionValue", e.target.value)}
- placeholder="옵션 값"
- className="border-0 focus-visible:ring-1 bg-transparent h-8"
- />
- </TableCell>
- <TableCell>
- <Input
- value={row.optionCode}
- onChange={(e) => updateNewRow(row.id, "optionCode", e.target.value)}
- placeholder="옵션 코드 (선택)"
- className="border-0 focus-visible:ring-1 bg-transparent h-8"
- />
- </TableCell>
- <TableCell>
- <Input
- type="number"
- value={row.sortOrder}
- onChange={(e) => updateNewRow(row.id, "sortOrder", parseInt(e.target.value) || 0)}
- className="border-0 focus-visible:ring-1 bg-transparent h-8"
- />
- </TableCell>
- <TableCell>
- <Button
- onClick={() => removeNewRow(row.id)}
- size="sm"
- variant="ghost"
- className="h-6 w-6 p-0"
- >
- <X className="h-3 w-3" />
- </Button>
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- <div className="p-3 border-t">
- <Button
- onClick={handleSaveNewOptions}
- size="sm"
- className="h-8"
- >
- <Save className="h-4 w-4 mr-1" />
- 저장
- </Button>
- </div>
- </div>
- )}
- </div>
-
- {/* 기존 하위 옵션 목록 */}
- <div className="space-y-2">
- <h4 className="text-sm font-medium">기존 하위 옵션</h4>
- <div className="border rounded-lg overflow-hidden">
- <Table>
- <TableHeader>
- <TableRow className="bg-muted/30">
- <TableHead className="w-[35%]">옵션 값</TableHead>
- <TableHead className="w-[25%]">옵션 코드</TableHead>
- <TableHead className="w-[15%]">순서</TableHead>
- <TableHead className="w-[15%]">상태</TableHead>
- <TableHead className="w-[10%]">작업</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {loading ? (
- <TableRow>
- <TableCell colSpan={5} className="text-center py-8">
- 로딩 중...
- </TableCell>
- </TableRow>
- ) : options.length === 0 ? (
- <TableRow>
- <TableCell colSpan={5} className="text-center text-muted-foreground py-8">
- 등록된 하위 옵션이 없습니다.
- </TableCell>
- </TableRow>
- ) : (
- options.map((option) => (
- <TableRow key={option.id} className="hover:bg-muted/30">
- <TableCell className="text-sm">
- {editingOption?.id === option.id ? (
- <Input
- value={editingOption.optionValue}
- onChange={(e) => setEditingOption(prev => prev ? { ...prev, optionValue: e.target.value } : null)}
- placeholder="옵션 값"
- className="border-0 focus-visible:ring-1 bg-transparent h-8"
- />
- ) : (
- option.optionValue
- )}
- </TableCell>
- <TableCell className="text-sm">
- {editingOption?.id === option.id ? (
- <Input
- value={editingOption.optionCode || ""}
- onChange={(e) => setEditingOption(prev => prev ? { ...prev, optionCode: e.target.value } : null)}
- placeholder="옵션 코드"
- className="border-0 focus-visible:ring-1 bg-transparent h-8"
- />
- ) : (
- option.optionCode || "-"
- )}
- </TableCell>
- <TableCell className="text-sm">
- {editingOption?.id === option.id ? (
- <Input
- type="number"
- value={editingOption.sortOrder}
- onChange={(e) => setEditingOption(prev => prev ? { ...prev, sortOrder: parseInt(e.target.value) || 0 } : null)}
- className="border-0 focus-visible:ring-1 bg-transparent h-8"
- />
- ) : (
- option.sortOrder
- )}
- </TableCell>
- <TableCell className="text-sm">
- <span className={`px-2 py-1 rounded text-xs ${
- option.isActive
- ? "bg-green-100 text-green-800"
- : "bg-red-100 text-red-800"
- }`}>
- {option.isActive ? "활성" : "비활성"}
- </span>
- </TableCell>
- <TableCell className="text-sm">
- {editingOption?.id === option.id ? (
- <div className="flex gap-1">
- <Button
- onClick={() => handleUpdateOption(editingOption)}
- size="sm"
- variant="outline"
- className="h-6 px-2 text-xs"
- >
- 저장
- </Button>
- <Button
- onClick={() => setEditingOption(null)}
- size="sm"
- variant="ghost"
- className="h-6 px-2 text-xs"
- >
- 취소
- </Button>
- </div>
- ) : (
- <div className="flex gap-1">
- <Button
- onClick={() => setEditingOption(option)}
- size="sm"
- variant="outline"
- className="h-6 px-2 text-xs"
- >
- 수정
- </Button>
- <Button
- onClick={() => handleDeleteOption(option.id)}
- size="sm"
- variant="ghost"
- className="h-6 px-2 text-xs text-red-600 hover:text-red-700"
- >
- <Trash2 className="h-3 w-3" />
- </Button>
- </div>
- )}
- </TableCell>
- </TableRow>
- ))
- )}
- </TableBody>
- </Table>
- </div>
- </div>
- </div>
- </SheetContent>
- </Sheet>
- )
-} \ No newline at end of file
diff --git a/lib/docu-list-rule/combo-box-settings/validation.ts b/lib/docu-list-rule/combo-box-settings/validation.ts
index a83651be..ca8e9192 100644
--- a/lib/docu-list-rule/combo-box-settings/validation.ts
+++ b/lib/docu-list-rule/combo-box-settings/validation.ts
@@ -1,12 +1,34 @@
-import { createSearchParamsCache } from "nuqs/server";
-import { parseAsInteger, parseAsString, parseAsArrayOf, parseAsStringEnum } from "nuqs/server";
-import { getSortingStateParser, getFiltersStateParser } from "@/lib/parsers";
+import {
+ createSearchParamsCache,
+ parseAsArrayOf,
+ parseAsInteger,
+ parseAsString,
+ parseAsStringEnum,
+} from "nuqs/server"
+import * as z from "zod"
-export const searchParamsComboBoxOptionsCache = createSearchParamsCache({
+import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"
+import { codeGroups } from "@/db/schema/docu-list-rule";
+
+export const searchParamsComboBoxSettingsCache = 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<typeof codeGroups.$inferSelect>().withDefault([
+ { id: "groupId", desc: false },
+ ]),
+
+ groupId: parseAsString.withDefault(""),
+ description: parseAsString.withDefault(""),
+ controlType: parseAsString.withDefault(""),
+ isActive: parseAsString.withDefault(""),
+
+ // advanced filter
+ filters: getFiltersStateParser().withDefault([]),
joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"),
-}); \ No newline at end of file
+ search: parseAsString.withDefault(""),
+})
+
+export type GetComboBoxSettingsSchema = Awaited<ReturnType<typeof searchParamsComboBoxSettingsCache.parse>> \ No newline at end of file
diff --git a/lib/docu-list-rule/document-class/service.ts b/lib/docu-list-rule/document-class/service.ts
index 99d85ea5..a1bb14a7 100644
--- a/lib/docu-list-rule/document-class/service.ts
+++ b/lib/docu-list-rule/document-class/service.ts
@@ -3,7 +3,7 @@
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 } from "drizzle-orm"
+import { eq, desc, asc, sql, and } from "drizzle-orm"
// Document Class 목록 조회 (A Class, B Class 등)
export async function getDocumentClassCodeGroups(input: {
@@ -69,7 +69,7 @@ export async function getDocumentClassCodeGroups(input: {
}
// 정렬 (안전한 필드 체크 적용)
- let orderBy = sql`${documentClasses.createdAt} DESC`
+ let orderBy = sql`${documentClasses.code} ASC`
if (sort && sort.length > 0) {
const sortField = sort[0]
// 안전성 체크: 필드가 실제 테이블에 존재하는지 확인
@@ -274,8 +274,34 @@ export async function updateDocumentClassCodeGroup(input: {
}
// Document Class 삭제
+// Document Class의 옵션 개수 조회
+export async function getDocumentClassOptionsCount(documentClassId: number) {
+ try {
+ const result = await db
+ .select({ count: sql<number>`count(*)` })
+ .from(documentClassOptions)
+ .where(eq(documentClassOptions.documentClassId, documentClassId))
+
+ return {
+ success: true,
+ count: result[0]?.count || 0
+ }
+ } catch (error) {
+ console.error("Error getting document class options count:", error)
+ return {
+ success: false,
+ count: 0
+ }
+ }
+}
+
export async function deleteDocumentClassCodeGroup(id: number) {
try {
+ // 먼저 해당 Document Class의 옵션들을 삭제
+ await db
+ .delete(documentClassOptions)
+ .where(eq(documentClassOptions.documentClassId, id))
+
// 삭제할 Document Class의 codeGroupId 확인
const documentClassToDelete = await db
.select({ codeGroupId: documentClasses.codeGroupId })
@@ -310,7 +336,7 @@ export async function deleteDocumentClassCodeGroup(id: number) {
return {
success: true,
data: deletedDocumentClass,
- message: "Document Class deleted successfully"
+ message: "Document Class and its options deleted successfully"
}
} catch (error) {
console.error("Error deleting document class:", error)
@@ -322,22 +348,57 @@ export async function deleteDocumentClassCodeGroup(id: number) {
}
// Document Class 옵션 목록 조회
-export async function getDocumentClassSubOptions(documentClassId: number) {
+export async function getDocumentClassSubOptions(documentClassId: number, input?: {
+ page?: number
+ perPage?: number
+ search?: string
+ sort?: Array<{ id: string; desc: boolean }>
+ filters?: Array<{ id: string; value: string }>
+ joinOperator?: "and" | "or"
+}) {
try {
+ const { page = 1, perPage = 1000, sort, search } = input || {}
+ const offset = (page - 1) * perPage
+
+ // 기본 조건: documentClassId
+ let whereConditions = eq(documentClassOptions.documentClassId, documentClassId)
+
+ // 검색 조건
+ if (search) {
+ const searchTerm = `%${search}%`
+ whereConditions = sql`${whereConditions} AND (
+ ${documentClassOptions.optionCode} ILIKE ${searchTerm} OR
+ ${documentClassOptions.description} ILIKE ${searchTerm}
+ )`
+ }
+
+ // 정렬 (안전한 필드 체크 적용)
+ let orderBy = sql`${documentClassOptions.optionCode} ASC`
+ if (sort && sort.length > 0) {
+ const sortField = sort[0]
+ // 안전성 체크: 필드가 실제 테이블에 존재하는지 확인
+ if (sortField && sortField.id && typeof sortField.id === "string" && sortField.id in documentClassOptions) {
+ const direction = sortField.desc ? sql`DESC` : sql`ASC`
+ const col = documentClassOptions[sortField.id as keyof typeof documentClassOptions]
+ orderBy = sql`${col} ${direction}`
+ }
+ }
+
const data = await db
.select({
id: documentClassOptions.id,
documentClassId: documentClassOptions.documentClassId,
- optionValue: documentClassOptions.optionValue,
+ description: documentClassOptions.description,
optionCode: documentClassOptions.optionCode,
- sortOrder: documentClassOptions.sortOrder,
isActive: documentClassOptions.isActive,
createdAt: documentClassOptions.createdAt,
updatedAt: documentClassOptions.updatedAt,
})
.from(documentClassOptions)
- .where(eq(documentClassOptions.documentClassId, documentClassId))
- .orderBy(asc(documentClassOptions.sortOrder), asc(documentClassOptions.optionValue))
+ .where(whereConditions)
+ .orderBy(orderBy)
+ .limit(perPage)
+ .offset(offset)
return {
success: true,
@@ -356,7 +417,7 @@ export async function getDocumentClassSubOptions(documentClassId: number) {
// Document Class 옵션 생성
export async function createDocumentClassOptionItem(input: {
documentClassId: number
- optionValue: string
+ optionCode: string
}) {
try {
// Document Class 정보 조회하여 Value 가져오기
@@ -373,30 +434,27 @@ export async function createDocumentClassOptionItem(input: {
}
}
- // Value에서 클래스명 추출 (예: "A Class" → "A")
- const classValue = documentClass[0].value
- const className = classValue.split(' ')[0] // "A Class"에서 "A" 추출
- // 자동으로 optionCode 생성 (예: "A_OP_01", "A_OP_02" 등)
- const existingOptions = await db
- .select({ optionCode: documentClassOptions.optionCode })
+
+ // 사용자가 입력한 코드를 그대로 사용
+ const userOptionCode = input.optionCode.toUpperCase().trim()
+
+ // 같은 Document Class 내에서 코드 중복 체크
+ const existingOption = await db
+ .select({ id: documentClassOptions.id })
.from(documentClassOptions)
- .where(eq(documentClassOptions.documentClassId, input.documentClassId))
- .orderBy(desc(documentClassOptions.optionCode))
-
- let newOptionCode = `${className}_OP_01`
- if (existingOptions.length > 0) {
- const lastOption = existingOptions[0]
- if (lastOption.optionCode) {
- // "A_OP_01" 형태에서 숫자 추출
- const match = lastOption.optionCode.match(/_OP_(\d+)$/)
- if (match) {
- const lastNumber = parseInt(match[1]) || 0
- newOptionCode = `${className}_OP_${String(lastNumber + 1).padStart(2, '0')}`
- } else {
- // 기존 형식이 다른 경우 01부터 시작
- newOptionCode = `${className}_OP_01`
- }
+ .where(
+ and(
+ eq(documentClassOptions.documentClassId, input.documentClassId),
+ eq(documentClassOptions.optionCode, userOptionCode)
+ )
+ )
+ .limit(1)
+
+ if (existingOption.length > 0) {
+ return {
+ success: false,
+ error: "이미 존재하는 코드입니다."
}
}
@@ -404,9 +462,8 @@ export async function createDocumentClassOptionItem(input: {
.insert(documentClassOptions)
.values({
documentClassId: input.documentClassId,
- optionValue: input.optionValue,
- optionCode: newOptionCode,
- sortOrder: 0,
+ description: userOptionCode, // 코드값을 description에도 자동 설정
+ optionCode: userOptionCode,
isActive: true,
})
.returning({ id: documentClassOptions.id })
@@ -430,13 +487,49 @@ export async function createDocumentClassOptionItem(input: {
// Document Class 옵션 수정
export async function updateDocumentClassOption(input: {
id: number
- optionValue: string
+ optionCode: string
}) {
try {
+ const userOptionCode = input.optionCode.toUpperCase().trim()
+
+ // 기존 옵션 조회하여 documentClassId 가져오기
+ const currentOption = await db
+ .select({ documentClassId: documentClassOptions.documentClassId })
+ .from(documentClassOptions)
+ .where(eq(documentClassOptions.id, input.id))
+ .limit(1)
+
+ if (!currentOption.length) {
+ return {
+ success: false,
+ error: "옵션을 찾을 수 없습니다."
+ }
+ }
+
+ // 같은 Document Class 내에서 코드 중복 체크 (자신 제외)
+ const existingOption = await db
+ .select({ id: documentClassOptions.id })
+ .from(documentClassOptions)
+ .where(
+ and(
+ eq(documentClassOptions.documentClassId, currentOption[0].documentClassId),
+ eq(documentClassOptions.optionCode, userOptionCode)
+ )
+ )
+ .limit(1)
+
+ if (existingOption.length > 0 && existingOption[0].id !== input.id) {
+ return {
+ success: false,
+ error: "이미 존재하는 코드입니다."
+ }
+ }
+
const [updatedOption] = await db
.update(documentClassOptions)
.set({
- optionValue: input.optionValue,
+ description: userOptionCode, // 코드값을 description에도 자동 설정
+ optionCode: userOptionCode,
updatedAt: new Date(),
})
.where(eq(documentClassOptions.id, input.id))
@@ -480,4 +573,4 @@ export async function deleteDocumentClassOption(id: number) {
error: "Failed to delete document class option"
}
}
-} \ No newline at end of file
+} \ No newline at end of file
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 677fe8ef..e81e4df6 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
@@ -28,8 +28,8 @@ import {
DrawerTrigger,
} from "@/components/ui/drawer"
-import { deleteDocumentClassCodeGroup } from "@/lib/docu-list-rule/document-class/service"
-import { documentClasses } from "@/db/schema/documentClasses"
+import { deleteDocumentClassCodeGroup, getDocumentClassOptionsCount } from "@/lib/docu-list-rule/document-class/service"
+import { documentClasses } from "@/db/schema"
interface DeleteDocumentClassDialogProps
extends React.ComponentPropsWithoutRef<typeof Dialog> {
@@ -45,8 +45,32 @@ export function DeleteDocumentClassDialog({
...props
}: DeleteDocumentClassDialogProps) {
const [isDeletePending, startDeleteTransition] = React.useTransition()
+ const [optionsCounts, setOptionsCounts] = React.useState<Record<number, number>>({})
+ const [isLoadingOptions, setIsLoadingOptions] = React.useState(false)
const isDesktop = useMediaQuery("(min-width: 640px)")
+ // Document Class들의 옵션 개수 조회
+ React.useEffect(() => {
+ const fetchOptionsCounts = async () => {
+ setIsLoadingOptions(true)
+ const counts: Record<number, number> = {}
+
+ for (const docClass of documentClasses) {
+ const result = await getDocumentClassOptionsCount(docClass.id)
+ if (result.success) {
+ counts[docClass.id] = result.count
+ }
+ }
+
+ setOptionsCounts(counts)
+ setIsLoadingOptions(false)
+ }
+
+ if (documentClasses.length > 0) {
+ fetchOptionsCounts()
+ }
+ }, [documentClasses])
+
function onDelete() {
startDeleteTransition(async () => {
try {
@@ -76,7 +100,7 @@ export function DeleteDocumentClassDialog({
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<Trash className="mr-2 size-4" aria-hidden="true" />
- 삭제 ({documentClasses.length})
+ delete ({documentClasses.length})
</Button>
</DialogTrigger>
) : null}
@@ -87,6 +111,26 @@ export function DeleteDocumentClassDialog({
이 작업은 되돌릴 수 없습니다. 선택된{" "}
<span className="font-medium">{documentClasses.length}</span>
개의 Document Class를 서버에서 영구적으로 삭제합니다.
+ {isLoadingOptions ? (
+ <div className="mt-2 text-sm text-muted-foreground">
+ 옵션 정보를 확인하는 중...
+ </div>
+ ) : (
+ documentClasses.some(docClass => optionsCounts[docClass.id] > 0) && (
+ <div className="mt-2 text-sm text-orange-600">
+ ⚠️ 다음 Document Class들은 옵션을 가지고 있어 함께 삭제됩니다:
+ <ul className="mt-1 ml-4 list-disc">
+ {documentClasses
+ .filter(docClass => optionsCounts[docClass.id] > 0)
+ .map(docClass => (
+ <li key={docClass.id}>
+ {docClass.code} ({optionsCounts[docClass.id]}개 옵션)
+ </li>
+ ))}
+ </ul>
+ </div>
+ )
+ )}
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2 sm:space-x-0">
@@ -123,15 +167,35 @@ export function DeleteDocumentClassDialog({
</Button>
</DrawerTrigger>
) : null}
- <DrawerContent>
- <DrawerHeader>
- <DrawerTitle>정말로 삭제하시겠습니까?</DrawerTitle>
- <DrawerDescription>
- 이 작업은 되돌릴 수 없습니다. 선택된{" "}
- <span className="font-medium">{documentClasses.length}</span>
- 개의 Document Class를 서버에서 영구적으로 삭제합니다.
- </DrawerDescription>
- </DrawerHeader>
+ <DrawerContent>
+ <DrawerHeader>
+ <DrawerTitle>정말로 삭제하시겠습니까?</DrawerTitle>
+ <DrawerDescription>
+ 이 작업은 되돌릴 수 없습니다. 선택된{" "}
+ <span className="font-medium">{documentClasses.length}</span>
+ 개의 Document Class를 서버에서 영구적으로 삭제합니다.
+ {isLoadingOptions ? (
+ <div className="mt-2 text-sm text-muted-foreground">
+ 옵션 정보를 확인하는 중...
+ </div>
+ ) : (
+ documentClasses.some(docClass => optionsCounts[docClass.id] > 0) && (
+ <div className="mt-2 text-sm text-orange-600">
+ ⚠️ 다음 Document Class들은 옵션을 가지고 있어 함께 삭제됩니다:
+ <ul className="mt-1 ml-4 list-disc">
+ {documentClasses
+ .filter(docClass => optionsCounts[docClass.id] > 0)
+ .map(docClass => (
+ <li key={docClass.id}>
+ {docClass.code} ({optionsCounts[docClass.id]}개 옵션)
+ </li>
+ ))}
+ </ul>
+ </div>
+ )
+ )}
+ </DrawerDescription>
+ </DrawerHeader>
<DrawerFooter className="gap-2 sm:space-x-0">
<DrawerClose asChild>
<Button variant="outline">취소</Button>
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 f0fcbc34..34ce239f 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/documentClasses"
+import { documentClassOptions } from "@/db/schema"
interface DeleteDocumentClassOptionDialogProps
extends React.ComponentPropsWithoutRef<typeof Dialog> {
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 97729caa..5ad23b22 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/documentClasses"
+import { documentClasses } from "@/db/schema"
const updateDocumentClassSchema = z.object({
value: z.string().min(1, "Value는 필수입니다."),
diff --git a/lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx b/lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx
index 5bfcbd33..93681c09 100644
--- a/lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx
+++ b/lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx
@@ -28,105 +28,93 @@ import {
import { Input } from "@/components/ui/input"
import { createDocumentClassOptionItem } from "@/lib/docu-list-rule/document-class/service"
-import { documentClasses } from "@/db/schema/documentClasses"
-const createDocumentClassOptionSchema = z.object({
- optionValue: z.string().min(1, "옵션 값은 필수입니다."),
+const createOptionSchema = z.object({
+ optionCode: z.string().min(1, "코드는 필수입니다."),
})
-type CreateDocumentClassOptionSchema = z.infer<typeof createDocumentClassOptionSchema>
+type CreateOptionSchema = z.infer<typeof createOptionSchema>
interface DocumentClassOptionAddDialogProps {
- selectedDocumentClass: typeof documentClasses.$inferSelect | null
+ documentClassId: number
onSuccess?: () => void
}
-export function DocumentClassOptionAddDialog({
- selectedDocumentClass,
- onSuccess,
-}: DocumentClassOptionAddDialogProps) {
+export function DocumentClassOptionAddDialog({ documentClassId, onSuccess }: DocumentClassOptionAddDialogProps) {
const [open, setOpen] = React.useState(false)
const [isPending, startTransition] = React.useTransition()
- const form = useForm<CreateDocumentClassOptionSchema>({
- resolver: zodResolver(createDocumentClassOptionSchema),
+ const form = useForm<CreateOptionSchema>({
+ resolver: zodResolver(createOptionSchema),
defaultValues: {
- optionValue: "",
+ optionCode: "",
},
- mode: "onChange"
})
- async function onSubmit(input: CreateDocumentClassOptionSchema) {
- if (!selectedDocumentClass) {
- toast.error("Document Class가 선택되지 않았습니다.")
- return
- }
-
+ const handleSubmit = (data: CreateOptionSchema) => {
startTransition(async () => {
try {
const result = await createDocumentClassOptionItem({
- documentClassId: selectedDocumentClass.id,
- optionValue: input.optionValue,
+ documentClassId,
+ optionCode: data.optionCode,
})
-
+
if (result.success) {
- toast.success("Document Class 옵션이 생성되었습니다.")
- form.reset()
+ toast.success("옵션이 성공적으로 추가되었습니다.")
setOpen(false)
+ form.reset()
onSuccess?.()
} else {
- toast.error(result.error || "생성에 실패했습니다.")
+ toast.error(`옵션 추가 실패: ${result.error}`)
}
} catch (error) {
console.error("Create error:", error)
- toast.error("Document Class 옵션 생성 중 오류가 발생했습니다.")
+ toast.error("옵션 추가 중 오류가 발생했습니다.")
}
})
}
const handleCancel = () => {
- form.reset()
setOpen(false)
+ form.reset()
}
return (
- <Dialog open={open && !!selectedDocumentClass} onOpenChange={setOpen}>
+ <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
- <Button variant="outline" size="sm" disabled={!selectedDocumentClass}>
+ <Button variant="outline" size="sm">
<Plus className="mr-2 h-4 w-4" />
옵션 추가
</Button>
</DialogTrigger>
- <DialogContent className="sm:max-w-[425px]">
+ <DialogContent>
<DialogHeader>
- <DialogTitle>Document Class 옵션 추가</DialogTitle>
+ <DialogTitle>옵션 추가</DialogTitle>
<DialogDescription>
- {selectedDocumentClass?.description || "Document Class"}에 새로운 옵션을 추가합니다.
- <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span>
+ 새로운 Document Class 옵션을 추가합니다.
</DialogDescription>
</DialogHeader>
<Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+ <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<FormField
control={form.control}
- name="optionValue"
+ name="optionCode"
render={({ field }) => (
<FormItem>
- <FormLabel>옵션 값 *</FormLabel>
+ <FormLabel>코드</FormLabel>
<FormControl>
- <Input {...field} placeholder="옵션 값을 입력하세요" />
+ <Input {...field} placeholder="옵션 코드" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
-
<DialogFooter>
<Button type="button" variant="outline" onClick={handleCancel}>
취소
</Button>
<Button type="submit" disabled={isPending || !form.formState.isValid}>
- {isPending ? "추가 중..." : "추가"}
+ 추가
</Button>
</DialogFooter>
</form>
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 6f6e7a87..bc2318c6 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
@@ -5,12 +5,10 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
-import { Loader } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Sheet,
- SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
@@ -26,14 +24,15 @@ import {
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
+
import { updateDocumentClassOption } from "@/lib/docu-list-rule/document-class/service"
-import { documentClassOptions } from "@/db/schema/documentClasses"
+import { documentClassOptions } from "@/db/schema"
-const updateDocumentClassOptionSchema = z.object({
- optionValue: z.string().min(1, "옵션 값은 필수입니다."),
+const updateOptionSchema = z.object({
+ optionCode: z.string().min(1, "코드는 필수입니다."),
})
-type UpdateDocumentClassOptionSchema = z.infer<typeof updateDocumentClassOptionSchema>
+type UpdateOptionSchema = z.infer<typeof updateOptionSchema>
interface DocumentClassOptionEditSheetProps {
open: boolean
@@ -48,91 +47,82 @@ export function DocumentClassOptionEditSheet({
data,
onSuccess,
}: DocumentClassOptionEditSheetProps) {
- const [isUpdatePending, startUpdateTransition] = React.useTransition()
+ const [isPending, startTransition] = React.useTransition()
- const form = useForm<UpdateDocumentClassOptionSchema>({
- resolver: zodResolver(updateDocumentClassOptionSchema),
+ const form = useForm<UpdateOptionSchema>({
+ resolver: zodResolver(updateOptionSchema),
defaultValues: {
- optionValue: data?.optionValue || "",
+ optionCode: "",
},
- mode: "onChange"
})
React.useEffect(() => {
if (data) {
form.reset({
- optionValue: data.optionValue || "",
+ optionCode: data.optionCode || "",
})
}
}, [data, form])
- async function onSubmit(input: UpdateDocumentClassOptionSchema) {
+ const handleSubmit = (formData: UpdateOptionSchema) => {
if (!data) return
- startUpdateTransition(async () => {
+ startTransition(async () => {
try {
const result = await updateDocumentClassOption({
id: data.id,
- optionValue: input.optionValue,
+ optionCode: formData.optionCode,
})
-
+
if (result.success) {
- toast.success("Document Class 옵션이 성공적으로 수정되었습니다.")
- onSuccess?.()
+ toast.success("옵션이 성공적으로 수정되었습니다.")
onOpenChange(false)
+ onSuccess?.()
} else {
- toast.error(result.error || "수정에 실패했습니다.")
+ toast.error(`옵션 수정 실패: ${result.error}`)
}
} catch (error) {
console.error("Update error:", error)
- toast.error("Document Class 옵션 수정 중 오류가 발생했습니다.")
+ toast.error("옵션 수정 중 오류가 발생했습니다.")
}
})
}
+ const handleCancel = () => {
+ onOpenChange(false)
+ form.reset()
+ }
+
return (
<Sheet open={open} onOpenChange={onOpenChange}>
- <SheetContent className="flex flex-col gap-6 sm:max-w-md">
- <SheetHeader className="text-left">
- <SheetTitle>Document Class 옵션 수정</SheetTitle>
+ <SheetContent>
+ <SheetHeader>
+ <SheetTitle>옵션 수정</SheetTitle>
<SheetDescription>
- Document Class 옵션 정보를 수정하고 변경사항을 저장하세요
+ Document Class 옵션을 수정합니다.
</SheetDescription>
</SheetHeader>
<Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
+ <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<FormField
control={form.control}
- name="optionValue"
+ name="optionCode"
render={({ field }) => (
<FormItem>
- <FormLabel>옵션 값</FormLabel>
+ <FormLabel>코드</FormLabel>
<FormControl>
- <Input placeholder="옵션 값을 입력하세요" {...field} />
+ <Input {...field} placeholder="옵션 코드" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
-
-
- <SheetFooter className="gap-2 pt-2 sm:space-x-0">
- <SheetClose asChild>
- <Button type="button" variant="outline">
- 취소
- </Button>
- </SheetClose>
- <Button
- type="submit"
- disabled={isUpdatePending || !form.formState.isValid}
- >
- {isUpdatePending && (
- <Loader
- className="mr-2 size-4 animate-spin"
- aria-hidden="true"
- />
- )}
- 저장
+ <SheetFooter>
+ <Button type="button" variant="outline" onClick={handleCancel}>
+ 취소
+ </Button>
+ <Button type="submit" disabled={isPending || !form.formState.isValid}>
+ 수정
</Button>
</SheetFooter>
</form>
diff --git a/lib/docu-list-rule/document-class/table/document-class-options-detail-sheet.tsx b/lib/docu-list-rule/document-class/table/document-class-options-detail-sheet.tsx
new file mode 100644
index 00000000..50e79d89
--- /dev/null
+++ b/lib/docu-list-rule/document-class/table/document-class-options-detail-sheet.tsx
@@ -0,0 +1,152 @@
+"use client"
+
+import * as React from "react"
+import { useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel } from "@tanstack/react-table"
+import { DataTableDetail } from "@/components/data-table/data-table-detail"
+import { DataTableAdvancedToolbarDetail } from "@/components/data-table/data-table-advanced-toolbar-detail"
+import type { DataTableAdvancedFilterField, DataTableRowAction } from "@/types/table"
+import {
+ Sheet,
+ SheetContent,
+} from "@/components/ui/sheet"
+import { getDocumentClassSubOptions } from "@/lib/docu-list-rule/document-class/service"
+import { getColumns } from "@/lib/docu-list-rule/document-class/table/document-class-options-table-columns"
+import { DocumentClassOptionEditSheet } from "@/lib/docu-list-rule/document-class/table/document-class-option-edit-sheet"
+import { DeleteDocumentClassOptionDialog } from "@/lib/docu-list-rule/document-class/table/delete-document-class-option-dialog"
+import { DocumentClassOptionsTableToolbarActions } from "@/lib/docu-list-rule/document-class/table/document-class-options-table-toolbar"
+import { documentClasses, documentClassOptions } from "@/db/schema/docu-list-rule"
+
+type DocumentClassOption = typeof documentClassOptions.$inferSelect
+
+interface DocumentClassOptionsDetailSheetProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ documentClass: typeof documentClasses.$inferSelect | null
+ onSuccess?: () => void
+ promises?: Promise<[{ data: DocumentClassOption[]; pageCount: number }]>
+}
+
+export function DocumentClassOptionsDetailSheet({
+ open,
+ onOpenChange,
+ documentClass,
+ promises,
+}: DocumentClassOptionsDetailSheetProps) {
+ const [rowAction, setRowAction] = React.useState<DataTableRowAction<DocumentClassOption> | null>(null)
+ const [rawData, setRawData] = React.useState<{ data: DocumentClassOption[]; pageCount: number }>({ data: [], pageCount: 0 })
+
+ React.useEffect(() => {
+ if (promises) {
+ promises.then(([result]) => {
+ setRawData(result)
+ })
+ } else if (open && documentClass) {
+ // fallback: 클라이언트에서 직접 fetch (CSR)
+ (async () => {
+ try {
+ const result = await getDocumentClassSubOptions(documentClass.id)
+ if (result.success && result.data) {
+ setRawData({
+ data: result.data,
+ pageCount: 1
+ })
+ }
+ } catch (error) {
+ console.error("Error refreshing data:", error)
+ }
+ })()
+ }
+ }, [promises, open, documentClass])
+
+ const refreshData = React.useCallback(async () => {
+ if (!documentClass) return
+
+ try {
+ const result = await getDocumentClassSubOptions(documentClass.id, {
+ page: 1,
+ perPage: 10,
+ })
+ if (result.success && result.data) {
+ setRawData({
+ data: result.data,
+ pageCount: 1
+ })
+ }
+ } catch (error) {
+ console.error("Error refreshing data:", error)
+ }
+ }, [documentClass])
+
+ const columns = React.useMemo(() => getColumns({ setRowAction }), [setRowAction])
+
+ // 고급 필터 필드 설정
+ const advancedFilterFields: DataTableAdvancedFilterField<DocumentClassOption>[] = [
+ { id: "optionCode", label: "코드", type: "text" },
+ { id: "description", label: "설명", type: "text" },
+ { id: "createdAt", label: "생성일", type: "date" },
+ ]
+
+ const table = useReactTable({
+ data: rawData.data,
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ initialState: {
+ sorting: [{ id: "optionCode", desc: false }],
+ pagination: {
+ pageSize: 10,
+ },
+ },
+ getRowId: (originalRow) => String(originalRow.id),
+ })
+
+ if (!documentClass) return null
+
+ return (
+ <Sheet open={open} onOpenChange={onOpenChange}>
+ <SheetContent className="flex flex-col gap-6 sm:max-w-4xl">
+ <div className="flex items-center justify-between">
+ <div>
+ <h3 className="text-lg font-medium">{documentClass.value} 옵션 관리</h3>
+ <p className="text-sm text-muted-foreground">
+ {documentClass.value}의 Document Class 옵션들을 관리합니다.
+ </p>
+ </div>
+ </div>
+
+ <DocumentClassOptionsTableToolbarActions
+ table={table}
+ documentClassId={documentClass.id}
+ onSuccess={refreshData}
+ />
+
+ <DataTableDetail table={table}>
+ <DataTableAdvancedToolbarDetail
+ table={table}
+ filterFields={advancedFilterFields}
+ />
+ </DataTableDetail>
+
+ <DeleteDocumentClassOptionDialog
+ open={rowAction?.type === "delete"}
+ onOpenChange={() => setRowAction(null)}
+ options={rowAction?.row.original ? [rowAction?.row.original] : []}
+ showTrigger={false}
+ onSuccess={() => {
+ rowAction?.row.toggleSelected(false)
+ refreshData()
+ }}
+ />
+
+ <DocumentClassOptionEditSheet
+ open={rowAction?.type === "update"}
+ onOpenChange={() => setRowAction(null)}
+ data={rowAction?.row.original ?? null}
+ onSuccess={refreshData}
+ />
+ </SheetContent>
+ </Sheet>
+ )
+} \ No newline at end of file
diff --git a/lib/docu-list-rule/document-class/table/document-class-options-table-columns.tsx b/lib/docu-list-rule/document-class/table/document-class-options-table-columns.tsx
index c04a7b37..c3bf440d 100644
--- a/lib/docu-list-rule/document-class/table/document-class-options-table-columns.tsx
+++ b/lib/docu-list-rule/document-class/table/document-class-options-table-columns.tsx
@@ -18,7 +18,7 @@ import {
} from "@/components/ui/dropdown-menu"
import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
-import { documentClassOptions } from "@/db/schema/documentClasses"
+import { documentClassOptions } from "@/db/schema/docu-list-rule"
interface GetColumnsProps {
setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<typeof documentClassOptions.$inferSelect> | null>>
@@ -114,19 +114,18 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof
minSize: 80
},
{
- accessorKey: "optionValue",
+ accessorKey: "description",
enableResizing: true,
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="옵션 값" />
+ <DataTableColumnHeaderSimple column={column} title="설명" />
),
meta: {
- excelHeader: "옵션 값",
+ excelHeader: "설명",
type: "text",
},
- cell: ({ row }) => row.getValue("optionValue") ?? "",
- minSize: 80
+ cell: ({ row }) => row.getValue("description") ?? "",
+ minSize: 120
},
-
{
accessorKey: "createdAt",
enableResizing: true,
@@ -141,7 +140,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof
const dateVal = row.getValue("createdAt") as Date
return formatDateTime(dateVal, "KR")
},
- minSize: 80
+ minSize: 100
}
]
diff --git a/lib/docu-list-rule/document-class/table/document-class-options-table-toolbar.tsx b/lib/docu-list-rule/document-class/table/document-class-options-table-toolbar.tsx
index 5044d90d..0cd44a4f 100644
--- a/lib/docu-list-rule/document-class/table/document-class-options-table-toolbar.tsx
+++ b/lib/docu-list-rule/document-class/table/document-class-options-table-toolbar.tsx
@@ -3,23 +3,23 @@
import * as React from "react"
import { type Table } from "@tanstack/react-table"
-import { DocumentClassOptionAddDialog } from "./document-class-option-add-dialog"
-import { DeleteDocumentClassOptionDialog } from "./delete-document-class-option-dialog"
-import { documentClasses, documentClassOptions } from "@/db/schema/documentClasses"
+import { DocumentClassOptionAddDialog } from "@/lib/docu-list-rule/document-class/table/document-class-option-add-dialog"
+import { DeleteDocumentClassOptionDialog } from "@/lib/docu-list-rule/document-class/table/delete-document-class-option-dialog"
+import { documentClassOptions } from "@/db/schema/docu-list-rule"
-interface DocumentClassOptionsTableToolbarActionsProps<TData> {
- table: Table<TData>
- selectedDocumentClass: typeof documentClasses.$inferSelect | null
+interface DocumentClassOptionsTableToolbarActionsProps {
+ table: Table<typeof documentClassOptions.$inferSelect>
+ documentClassId: number
onSuccess?: () => void
}
-export function DocumentClassOptionsTableToolbarActions<TData>({
+export function DocumentClassOptionsTableToolbarActions({
table,
- selectedDocumentClass,
+ documentClassId,
onSuccess,
-}: DocumentClassOptionsTableToolbarActionsProps<TData>) {
+}: DocumentClassOptionsTableToolbarActionsProps) {
const selectedRows = table.getFilteredSelectedRowModel().rows
- const selectedOptions = selectedRows.map((row) => row.original as typeof documentClassOptions.$inferSelect)
+ const selectedOptions = selectedRows.map((row) => row.original)
return (
<div className="flex items-center gap-2">
@@ -35,7 +35,7 @@ export function DocumentClassOptionsTableToolbarActions<TData>({
) : null}
<DocumentClassOptionAddDialog
- selectedDocumentClass={selectedDocumentClass}
+ documentClassId={documentClassId}
onSuccess={onSuccess}
/>
</div>
diff --git a/lib/docu-list-rule/document-class/table/document-class-options-table.tsx b/lib/docu-list-rule/document-class/table/document-class-options-table.tsx
deleted file mode 100644
index 644e3599..00000000
--- a/lib/docu-list-rule/document-class/table/document-class-options-table.tsx
+++ /dev/null
@@ -1,176 +0,0 @@
-"use client"
-
-import * as React from "react"
-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, DataTableRowAction } from "@/types/table"
-
-import { getDocumentClassSubOptions } from "@/lib/docu-list-rule/document-class/service"
-import { getColumns } from "./document-class-options-table-columns"
-import { DocumentClassOptionEditSheet } from "./document-class-option-edit-sheet"
-import { DeleteDocumentClassOptionDialog } from "./delete-document-class-option-dialog"
-import { DocumentClassOptionsTableToolbarActions } from "./document-class-options-table-toolbar"
-import { documentClasses, documentClassOptions } from "@/db/schema/docu-list-rule"
-
-type DocumentClass = typeof documentClasses.$inferSelect
-
-interface DocumentClassOptionsTableProps {
- selectedDocumentClass: DocumentClass | null
- documentClasses: DocumentClass[]
- onSelectDocumentClass: (documentClass: DocumentClass) => void
-}
-
-export function DocumentClassOptionsTable({
- selectedDocumentClass,
- documentClasses,
- onSelectDocumentClass
-}: DocumentClassOptionsTableProps) {
- const [rowAction, setRowAction] = React.useState<DataTableRowAction<typeof documentClassOptions.$inferSelect> | null>(null)
-
- // 선택된 Document Class의 옵션 데이터 로드
- const [options, setOptions] = React.useState<typeof documentClassOptions.$inferSelect[]>([])
-
- // DB 등록 순서대로 정렬된 Document Classes
- const sortedDocumentClasses = React.useMemo(() => {
- return [...documentClasses].sort((a, b) => a.id - b.id)
- }, [documentClasses])
-
- const handleSuccess = React.useCallback(async () => {
- // 옵션 테이블 새로고침
- if (selectedDocumentClass) {
- try {
- const result = await getDocumentClassSubOptions(selectedDocumentClass.id)
- if (result.success && result.data) {
- setOptions(result.data)
- }
- } catch (error) {
- console.error("Error refreshing options:", error)
- }
- }
- }, [selectedDocumentClass])
-
- const columns = React.useMemo(() => getColumns({ setRowAction }), [setRowAction])
-
- // 고급 필터 필드 설정
- const advancedFilterFields: DataTableAdvancedFilterField<typeof documentClassOptions.$inferSelect>[] = [
- { id: "optionCode", label: "코드", type: "text" },
- { id: "optionValue", label: "옵션 값", type: "text" },
- { id: "createdAt", label: "생성일", type: "date" },
- ]
-
- const { table } = useDataTable({
- data: options,
- columns,
- pageCount: 1,
- enablePinning: true,
- enableAdvancedFilter: true,
- manualSorting: false,
- initialState: {
- sorting: [{ id: "id", desc: false }],
- columnPinning: { right: ["actions"] },
- },
- getRowId: (originalRow) => String(originalRow.id),
- shallow: false,
- clearOnDefault: true,
- })
-
- React.useEffect(() => {
- const loadOptions = async () => {
- if (!selectedDocumentClass) {
- setOptions([])
- return
- }
-
- try {
- const result = await getDocumentClassSubOptions(selectedDocumentClass.id)
- if (result.success && result.data) {
- setOptions(result.data)
- }
- } catch (error) {
- console.error("Error loading options:", error)
- setOptions([])
- }
- }
-
- loadOptions()
- }, [selectedDocumentClass])
-
- if (!selectedDocumentClass) {
- return (
- <div className="space-y-4">
- <div className="flex gap-2">
- {sortedDocumentClasses.map((documentClass) => (
- <button
- key={documentClass.id}
- onClick={() => onSelectDocumentClass(documentClass)}
- className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
- selectedDocumentClass?.id === documentClass.id
- ? "bg-primary text-primary-foreground"
- : "bg-muted text-muted-foreground hover:bg-muted/80"
- }`}
- >
- {documentClass.value}
- </button>
- ))}
- </div>
- <div className="text-center text-muted-foreground py-4">
- Document Class를 선택하면 옵션을 관리할 수 있습니다.
- </div>
- </div>
- )
- }
-
- return (
- <>
- <div className="space-y-2">
- <div className="flex gap-2">
- {sortedDocumentClasses.map((documentClass) => (
- <button
- key={documentClass.id}
- onClick={() => onSelectDocumentClass(documentClass)}
- className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
- selectedDocumentClass?.id === documentClass.id
- ? "bg-primary text-primary-foreground"
- : "bg-muted text-muted-foreground hover:bg-muted/80"
- }`}
- >
- {documentClass.value}
- </button>
- ))}
- </div>
-
- <DataTable table={table}>
- <DataTableAdvancedToolbar
- table={table}
- filterFields={advancedFilterFields}
- >
- <DocumentClassOptionsTableToolbarActions
- table={table}
- selectedDocumentClass={selectedDocumentClass}
- onSuccess={handleSuccess}
- />
- </DataTableAdvancedToolbar>
- </DataTable>
- </div>
-
- <DeleteDocumentClassOptionDialog
- open={rowAction?.type === "delete"}
- onOpenChange={() => setRowAction(null)}
- options={rowAction?.row.original ? [rowAction?.row.original] : []}
- showTrigger={false}
- onSuccess={() => {
- rowAction?.row.toggleSelected(false)
- handleSuccess()
- }}
- />
-
- <DocumentClassOptionEditSheet
- open={rowAction?.type === "update"}
- onOpenChange={() => setRowAction(null)}
- data={rowAction?.row.original ?? null}
- onSuccess={handleSuccess}
- />
- </>
- )
-} \ No newline at end of file
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 6684d13a..8c391def 100644
--- a/lib/docu-list-rule/document-class/table/document-class-table-columns.tsx
+++ b/lib/docu-list-rule/document-class/table/document-class-table-columns.tsx
@@ -22,12 +22,13 @@ import { documentClasses } from "@/db/schema/docu-list-rule"
interface GetColumnsProps {
setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<typeof documentClasses.$inferSelect> | null>>
+ onDetail?: (documentClass: typeof documentClasses.$inferSelect) => void
}
/**
* tanstack table 컬럼 정의
*/
-export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof documentClasses.$inferSelect>[] {
+export function getColumns({ setRowAction, onDetail }: GetColumnsProps): ColumnDef<typeof documentClasses.$inferSelect>[] {
// ----------------------------------------------------------------
// 1) select 컬럼 (체크박스)
// ----------------------------------------------------------------
@@ -77,6 +78,11 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem
+ onSelect={() => onDetail?.(row.original)}
+ >
+ Detail
+ </DropdownMenuItem>
+ <DropdownMenuItem
onSelect={() => setRowAction({ row, type: "update" })}
>
Edit
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 7bc28a06..9b43f43d 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
@@ -5,7 +5,7 @@ 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/documentClasses"
+import { documentClasses } from "@/db/schema"
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 e3daac8a..c66a1395 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
@@ -6,12 +6,11 @@ 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, DataTableRowAction } from "@/types/table"
-import { getDocumentClassCodeGroups } from "@/lib/docu-list-rule/document-class/service"
-import { getColumns } from "./document-class-table-columns"
-import { DocumentClassEditSheet } from "./document-class-edit-sheet"
-import { DocumentClassOptionsTable } from "./document-class-options-table"
-import { DocumentClassTableToolbarActions } from "./document-class-table-toolbar"
-import { DeleteDocumentClassDialog } from "./delete-document-class-dialog"
+import { getColumns } from "@/lib/docu-list-rule/document-class/table/document-class-table-columns"
+import { DocumentClassEditSheet } from "@/lib/docu-list-rule/document-class/table/document-class-edit-sheet"
+import { DocumentClassOptionsDetailSheet } from "@/lib/docu-list-rule/document-class/table/document-class-options-detail-sheet"
+import { DocumentClassTableToolbarActions } from "@/lib/docu-list-rule/document-class/table/document-class-table-toolbar"
+import { DeleteDocumentClassDialog } from "@/lib/docu-list-rule/document-class/table/delete-document-class-dialog"
import { documentClasses } from "@/db/schema/docu-list-rule"
interface DocumentClassTableProps {
@@ -22,6 +21,7 @@ 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 [isDetailSheetOpen, setIsDetailSheetOpen] = React.useState(false)
const [selectedDocumentClass, setSelectedDocumentClass] = React.useState<typeof documentClasses.$inferSelect | null>(null)
const refreshData = React.useCallback(() => {
@@ -29,7 +29,13 @@ export function DocumentClassTable({ promises }: DocumentClassTableProps) {
router.refresh()
}, [router])
- const columns = React.useMemo(() => getColumns({ setRowAction }), [setRowAction])
+ // Detail 버튼 클릭 핸들러
+ const handleDetail = React.useCallback((documentClass: typeof documentClasses.$inferSelect) => {
+ setSelectedDocumentClass(documentClass)
+ setIsDetailSheetOpen(true)
+ }, [])
+
+ const columns = React.useMemo(() => getColumns({ setRowAction, onDetail: handleDetail }), [setRowAction, handleDetail])
// 고급 필터 필드 설정
const advancedFilterFields: DataTableAdvancedFilterField<typeof documentClasses.$inferSelect>[] = [
@@ -40,14 +46,12 @@ export function DocumentClassTable({ promises }: DocumentClassTableProps) {
]
const { table } = useDataTable({
- data: rawData[0].data as any,
+ data: rawData[0].data as typeof documentClasses.$inferSelect[],
columns,
pageCount: rawData[0].pageCount,
enablePinning: true,
enableAdvancedFilter: true,
- manualSorting: false,
initialState: {
- sorting: [{ id: "createdAt", desc: true }],
columnPinning: { right: ["actions"] },
},
getRowId: (originalRow) => String(originalRow.id),
@@ -66,26 +70,15 @@ export function DocumentClassTable({ promises }: DocumentClassTableProps) {
</DataTableAdvancedToolbar>
</DataTable>
- {/* 구분선 */}
- <div className="border-t border-border my-6"></div>
-
- {/* Document Class 옵션 관리 제목 */}
- <div className="flex items-center justify-between space-y-2">
- <div>
- <div className="flex items-center gap-2">
- <h2 className="text-2xl font-bold tracking-tight">Document Class 옵션 관리</h2>
- </div>
- <p className="text-muted-foreground">
- Document Class 옵션들을 관리합니다.
- </p>
- </div>
- </div>
-
- {/* Document Class 옵션 테이블 */}
- <DocumentClassOptionsTable
- selectedDocumentClass={selectedDocumentClass}
- documentClasses={rawData[0].data || []}
- onSelectDocumentClass={setSelectedDocumentClass}
+ {/* Detail 시트 */}
+ <DocumentClassOptionsDetailSheet
+ open={isDetailSheetOpen}
+ onOpenChange={setIsDetailSheetOpen}
+ documentClass={selectedDocumentClass}
+ onSuccess={() => {
+ setIsDetailSheetOpen(false)
+ setSelectedDocumentClass(null)
+ }}
/>
<DeleteDocumentClassDialog
diff --git a/lib/docu-list-rule/document-class/validation.ts b/lib/docu-list-rule/document-class/validation.ts
index 0600e8fb..78f87484 100644
--- a/lib/docu-list-rule/document-class/validation.ts
+++ b/lib/docu-list-rule/document-class/validation.ts
@@ -1,5 +1,5 @@
import { createSearchParamsCache } from "nuqs/server";
-import { parseAsInteger, parseAsString, parseAsArrayOf, parseAsStringEnum } from "nuqs/server";
+import { parseAsInteger, parseAsString, parseAsStringEnum } from "nuqs/server";
import { getSortingStateParser, getFiltersStateParser } from "@/lib/parsers";
export const searchParamsDocumentClassCache = createSearchParamsCache({
diff --git a/lib/docu-list-rule/number-type-configs/repository.ts b/lib/docu-list-rule/number-type-configs/repository.ts
deleted file mode 100644
index 56be1bcf..00000000
--- a/lib/docu-list-rule/number-type-configs/repository.ts
+++ /dev/null
@@ -1,269 +0,0 @@
-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 1ba9b8f0..0b0bb905 100644
--- a/lib/docu-list-rule/number-type-configs/service.ts
+++ b/lib/docu-list-rule/number-type-configs/service.ts
@@ -3,111 +3,176 @@
import { revalidatePath } from "next/cache"
import db from "@/db/db"
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 { documentNumberTypeConfigs, codeGroups } from "@/db/schema/docu-list-rule"
+import { asc, eq, sql, count, and } from "drizzle-orm"
import { GetNumberTypeConfigsSchema } from "./validation"
-// 특정 Number Type의 Configs 조회 (고급 필터링 지원)
+// 특정 Number Type의 Configs 조회
export async function getNumberTypeConfigs(input: GetNumberTypeConfigsSchema) {
- return unstable_cache(
- async () => {
- try {
- const offset = (input.page - 1) * input.perPage
+ unstable_noStore()
+
+ try {
+ const { numberTypeId, page = 1, perPage = 10, search, filters, joinOperator } = input
+ const offset = (page - 1) * perPage
- // 고급 필터링 지원
- const advancedWhere = filterColumns({
- table: documentNumberTypeConfigs,
- filters: input.filters,
- joinOperator: input.joinOperator,
- })
+ // numberTypeId 유효성 검사
+ if (!numberTypeId || numberTypeId <= 0) {
+ return {
+ success: true,
+ data: [],
+ pageCount: 0,
+ }
+ }
- // 정렬 처리
- 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)]
+ // 기본 조건: 특정 Number Type
+ let whereConditions = eq(documentNumberTypeConfigs.documentNumberTypeId, numberTypeId)
- // 트랜잭션 내부에서 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,
- })
+ // 검색 조건 추가
+ if (search && search.trim() !== "") {
+ const searchTerm = `%${search}%`
+ whereConditions = sql`${whereConditions} AND (
+ ${codeGroups.description} ILIKE ${searchTerm} OR
+ ${documentNumberTypeConfigs.description} ILIKE ${searchTerm} OR
+ ${documentNumberTypeConfigs.remark} ILIKE ${searchTerm}
+ )`
+ }
- const total = await countNumberTypeConfigs(tx, {
- numberTypeId: input.numberTypeId,
- where: advancedWhere,
- search: input.search || undefined,
- })
+ // 고급 필터링 (Code Groups 스타일)
+ if (filters && filters.length > 0) {
+ const filterConditions = filters.map(filter => {
+ const { id, value } = filter
+ if (!value || Array.isArray(value)) return null
+
+ switch (id) {
+ case "codeGroupName":
+ return sql`${codeGroups.description} ILIKE ${`%${value}%`}`
+ case "description":
+ return sql`${documentNumberTypeConfigs.description} ILIKE ${`%${value}%`}`
+ case "remark":
+ return sql`${documentNumberTypeConfigs.remark} ILIKE ${`%${value}%`}`
+ case "sdq":
+ return sql`${documentNumberTypeConfigs.sdq} = ${parseInt(value)}`
+ case "isActive":
+ return sql`${documentNumberTypeConfigs.isActive} = ${value === "true"}`
+ default:
+ return null
+ }
+ }).filter(Boolean)
- return { data, total }
+ 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 = and(whereConditions, sql`(${combinedFilters})`) || whereConditions
+ }
+ }
- 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,
+ // 정렬 (Code Groups 스타일)
+ let orderBy = sql`${documentNumberTypeConfigs.sdq} ASC`
+ if (input.sort && input.sort.length > 0) {
+ const sortField = input.sort[0]
+ // 안전성 체크: 필드가 실제 테이블에 존재하는지 확인
+ if (sortField && sortField.id && typeof sortField.id === "string") {
+ const direction = sortField.desc ? sql`DESC` : sql`ASC`
+
+ // 정렬 필드에 따른 컬럼 선택
+ let col: typeof documentNumberTypeConfigs.sdq | typeof documentNumberTypeConfigs.description | typeof documentNumberTypeConfigs.remark | typeof codeGroups.description | typeof documentNumberTypeConfigs.createdAt | typeof documentNumberTypeConfigs.updatedAt
+ switch (sortField.id) {
+ case "sdq":
+ col = documentNumberTypeConfigs.sdq
+ break
+ case "description":
+ col = documentNumberTypeConfigs.description
+ break
+ case "remark":
+ col = documentNumberTypeConfigs.remark
+ break
+ case "codeGroupName":
+ col = codeGroups.description
+ break
+ case "createdAt":
+ col = documentNumberTypeConfigs.createdAt
+ break
+ case "updatedAt":
+ col = documentNumberTypeConfigs.updatedAt
+ break
+ default:
+ col = documentNumberTypeConfigs.sdq
}
+
+ orderBy = sql`${col} ${direction}`
}
- },
- [JSON.stringify(input)], // 캐싱 키
- {
- revalidate: 300, // 5분 캐시
- tags: ["number-type-configs"],
}
- )()
+
+ // 데이터 조회
+ const data = await db
+ .select({
+ id: documentNumberTypeConfigs.id,
+ documentNumberTypeId: documentNumberTypeConfigs.documentNumberTypeId,
+ codeGroupId: documentNumberTypeConfigs.codeGroupId,
+ sdq: documentNumberTypeConfigs.sdq,
+ description: documentNumberTypeConfigs.description,
+ remark: documentNumberTypeConfigs.remark,
+ isActive: documentNumberTypeConfigs.isActive,
+ createdAt: documentNumberTypeConfigs.createdAt,
+ updatedAt: documentNumberTypeConfigs.updatedAt,
+ codeGroupName: codeGroups.description,
+ codeGroupControlType: codeGroups.controlType,
+ })
+ .from(documentNumberTypeConfigs)
+ .leftJoin(codeGroups, eq(documentNumberTypeConfigs.codeGroupId, codeGroups.id))
+ .where(whereConditions)
+ .orderBy(orderBy)
+ .limit(perPage)
+ .offset(offset)
+
+ // 총 개수 조회
+ const [{ count: total }] = await db
+ .select({ count: count() })
+ .from(documentNumberTypeConfigs)
+ .leftJoin(codeGroups, eq(documentNumberTypeConfigs.codeGroupId, codeGroups.id))
+ .where(whereConditions)
+
+ const pageCount = Math.ceil(total / 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,
+ }
+ }
}
// Number Type Config 생성
export async function createNumberTypeConfig(input: {
documentNumberTypeId: number
codeGroupId: number | null
- documentClassId: number | null
sdq: number
description?: string
remark?: string
}) {
try {
- const result = await db.transaction(async (tx) => {
- const [newConfig] = await insertNumberTypeConfig(tx, input)
- return newConfig
- })
+ const [result] = await db
+ .insert(documentNumberTypeConfigs)
+ .values({
+ documentNumberTypeId: input.documentNumberTypeId,
+ codeGroupId: input.codeGroupId,
+ sdq: input.sdq,
+ description: input.description,
+ remark: input.remark,
+ })
+ .returning({ id: documentNumberTypeConfigs.id })
revalidatePath("/evcp/docu-list-rule/number-type-configs")
@@ -129,22 +194,22 @@ export async function createNumberTypeConfig(input: {
export async function updateNumberTypeConfig(input: {
id: number
codeGroupId: number | null
- documentClassId: number | null
sdq: number
description?: string
remark?: string
}) {
try {
- const result = await db.transaction(async (tx) => {
- const [updatedConfig] = await updateNumberTypeConfigById(tx, input.id, {
+ const [result] = await db
+ .update(documentNumberTypeConfigs)
+ .set({
codeGroupId: input.codeGroupId,
- documentClassId: input.documentClassId,
sdq: input.sdq,
description: input.description,
remark: input.remark,
+ updatedAt: new Date(),
})
- return updatedConfig
- })
+ .where(eq(documentNumberTypeConfigs.id, input.id))
+ .returning({ id: documentNumberTypeConfigs.id })
revalidatePath("/evcp/docu-list-rule/number-type-configs")
@@ -165,29 +230,40 @@ export async function updateNumberTypeConfig(input: {
// Number Type Config 삭제
export async function deleteNumberTypeConfig(id: number) {
try {
- await db.transaction(async (tx) => {
- // 삭제할 항목의 정보를 먼저 가져옴
- const [configToDelete] = await selectConfigToDelete(tx, id)
+ // 삭제할 Config 정보 조회
+ const [configToDelete] = await db
+ .select({
+ documentNumberTypeId: documentNumberTypeConfigs.documentNumberTypeId,
+ sdq: documentNumberTypeConfigs.sdq,
+ })
+ .from(documentNumberTypeConfigs)
+ .where(eq(documentNumberTypeConfigs.id, id))
- if (!configToDelete) {
- throw new Error("Config not found")
- }
+ if (!configToDelete) {
+ throw new Error("Config not found")
+ }
- // 항목 삭제
- await deleteNumberTypeConfigById(tx, id)
+ // Config 삭제
+ await db
+ .delete(documentNumberTypeConfigs)
+ .where(eq(documentNumberTypeConfigs.id, id))
- // 같은 Number Type의 남은 항목들 중에서 삭제된 항목보다 큰 순서를 가진 항목들의 순서를 1씩 감소
- await reorderConfigsAfterDelete(
- tx,
- configToDelete.documentNumberTypeId,
- configToDelete.sdq
+ // 순서 재정렬 (삭제된 순서보다 큰 모든 항목의 순서를 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}`
)
- })
revalidatePath("/evcp/docu-list-rule/number-type-configs")
return {
success: true,
+ data: configToDelete,
message: "Number Type Config deleted successfully"
}
} catch (error) {
@@ -212,7 +288,17 @@ export async function getActiveCodeGroups() {
unstable_noStore()
try {
- const codeGroupsData = await selectActiveCodeGroups(db)
+ 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))
return {
success: true,
@@ -228,43 +314,4 @@ export async function getActiveCodeGroups() {
}
}
-// Document Class Code Group ID 조회
-export async function getDocumentClassCodeGroupId() {
- try {
- const [codeGroup] = await selectDocumentClassCodeGroupId(db)
- return codeGroup?.id || null
- } catch (error) {
- console.error("Error fetching document class code group id:", error)
- return null
- }
-}
-
-// 활성화된 Document Classes 조회 (Config 생성/수정 시 사용)
-export async function getActiveDocumentClasses() {
- unstable_noStore()
-
- try {
- const documentClassesData = await db
- .select({
- id: documentClasses.id,
- code: documentClasses.code,
- value: documentClasses.value,
- description: documentClasses.description,
- })
- .from(documentClasses)
- .where(eq(documentClasses.isActive, true))
- .orderBy(asc(documentClasses.value))
-
- return {
- success: true,
- data: documentClassesData,
- }
- } catch (error) {
- console.error("Error fetching active document classes:", error)
- return {
- success: false,
- error: "Failed to fetch active document classes",
- data: [],
- }
- }
-} \ No newline at end of file
+ \ No newline at end of file
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 9ed8ca71..96c7e7c7 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
@@ -78,7 +78,7 @@ export function DeleteNumberTypeConfigsDialog({
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<Trash className="mr-2 size-4" aria-hidden="true" />
- 삭제 ({configs.length})
+ delete ({configs.length})
</Button>
</DialogTrigger>
) : null}
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 72ee2698..69d84a3f 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
@@ -100,7 +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 3b3d0180..3e4dd262 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
@@ -62,6 +62,7 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon
// 첫 번째 타입의 configs 데이터도 바로 로드
await fetchConfigs(firstNumberTypeId)
}
+
} catch (error) {
console.error("Error in loadData:", error)
}
@@ -69,6 +70,7 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon
loadData()
}, [rawData, fetchConfigs])
+
// Number Type 변경 핸들러 (서버 사이드 처리 지원)
const handleNumberTypeChange = React.useCallback((numberTypeId: number) => {
@@ -102,7 +104,7 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon
await updateNumberTypeConfig({
id: config.id,
codeGroupId: config.codeGroupId,
- documentClassId: config.documentClassId,
+
sdq: config.sdq,
description: config.description || undefined,
remark: config.remark || undefined,
@@ -142,15 +144,13 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon
pageCount: configsData.pageCount,
enablePinning: true,
enableAdvancedFilter: true,
- manualSorting: true,
- manualFiltering: true,
- manualPagination: true,
+ enableGlobalFilter: true, // 전역 검색 활성화
initialState: {
sorting: [{ id: "sdq", desc: false }],
},
getRowId: (row) => String(row.id),
shallow: false,
- clearOnDefault: true,
+ clearOnDefault: true, // 검색어 지울 때 URL 파라미터도 지우도록 true로 변경
})
const refreshData = React.useCallback(async () => {
@@ -159,18 +159,44 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon
}
}, [selectedNumberType, fetchConfigs])
- // 테이블 상태가 변경될 때 서버 데이터 다시 로드
+ // 테이블 상태가 변경될 때 서버 데이터 다시 로드 (디바운싱 적용)
+ const [lastSearchTerm, setLastSearchTerm] = React.useState<string>("")
+
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 || "",
- })
+ const currentSearchTerm = state.globalFilter || ""
+
+ console.log("🔍 useEffect triggered - currentSearchTerm:", `"${currentSearchTerm}"`, "lastSearchTerm:", `"${lastSearchTerm}"`)
+
+ // 검색어가 실제로 변경되었을 때만 API 호출
+ if (currentSearchTerm !== lastSearchTerm) {
+ console.log("🔍 Search term changed from:", `"${lastSearchTerm}"`, "to:", `"${currentSearchTerm}"`)
+ setLastSearchTerm(currentSearchTerm)
+
+ 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: currentSearchTerm,
+ })
+ } else {
+ console.log("🔍 Search term unchanged, skipping API call")
+ }
+ }
+ }, [table, selectedNumberType, fetchConfigs, lastSearchTerm])
+
+ // URL 파라미터에서 검색어를 가져와서 테이블 상태 동기화
+ React.useEffect(() => {
+ console.log("🔍 URL searchParams.search:", searchParams.search)
+ if (searchParams.search && searchParams.search.trim() !== "") {
+ console.log("🔍 Syncing search from URL:", searchParams.search)
+ table.setGlobalFilter(searchParams.search)
+ } else if (searchParams.search === undefined || searchParams.search === "") {
+ console.log("🔍 Clearing search filter from URL")
+ table.setGlobalFilter("")
}
- }, [table, selectedNumberType, fetchConfigs])
+ }, [searchParams.search, table])
return (
<>
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 4923bd1d..b30ee268 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
@@ -108,7 +108,7 @@ export function NumberTypeConfigsToolbarActions({
const result = await createNumberTypeConfig({
documentNumberTypeId: selectedNumberType,
codeGroupId: codeGroupId,
- documentClassId: null,
+
sdq: sdq,
description: formData.description || undefined,
remark: formData.remark || undefined,
diff --git a/lib/docu-list-rule/number-type-configs/validation.ts b/lib/docu-list-rule/number-type-configs/validation.ts
index 02b99290..deb8193a 100644
--- a/lib/docu-list-rule/number-type-configs/validation.ts
+++ b/lib/docu-list-rule/number-type-configs/validation.ts
@@ -1,11 +1,19 @@
-import { createSearchParamsCache } from "nuqs/server";
-import { parseAsInteger, parseAsString, parseAsArrayOf, parseAsStringEnum } from "nuqs/server";
-import { getSortingStateParser, getFiltersStateParser } from "@/lib/parsers";
+import {
+ createSearchParamsCache,
+ parseAsArrayOf,
+ parseAsInteger,
+ parseAsString,
+ parseAsStringEnum,
+} from "nuqs/server"
import * as z from "zod"
-import { NumberTypeConfig } from "../types";
+
+import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"
+import { NumberTypeConfig } from "../types"
export const searchParamsNumberTypeConfigsCache = createSearchParamsCache({
- flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]),
+ flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault(
+ []
+ ),
page: parseAsInteger.withDefault(1),
perPage: parseAsInteger.withDefault(10),
sort: getSortingStateParser<NumberTypeConfig>().withDefault([
@@ -13,7 +21,7 @@ export const searchParamsNumberTypeConfigsCache = createSearchParamsCache({
]),
// Number Type ID (필수)
- numberTypeId: parseAsInteger.withDefault(0),
+ numberTypeId: parseAsInteger.withDefault(1),
// 기본 필터들
codeGroupName: parseAsString.withDefault(""),
@@ -25,6 +33,6 @@ export const searchParamsNumberTypeConfigsCache = createSearchParamsCache({
filters: getFiltersStateParser().withDefault([]),
joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"),
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 17373848..12c3bf73 100644
--- a/lib/docu-list-rule/number-types/service.ts
+++ b/lib/docu-list-rule/number-types/service.ts
@@ -69,7 +69,7 @@ export async function getNumberTypes(input: {
}
// 정렬 (안전한 필드 체크 적용)
- let orderBy = sql`${documentNumberTypes.createdAt} DESC`
+ let orderBy = sql`${documentNumberTypes.name} ASC`
if (sort && sort.length > 0) {
const sortField = sort[0]
// 안전성 체크: 필드가 실제 테이블에 존재하는지 확인
diff --git a/lib/docu-list-rule/number-types/table/delete-number-types-dialog.tsx b/lib/docu-list-rule/number-types/table/delete-number-types-dialog.tsx
index 8d1bc679..f0d80350 100644
--- a/lib/docu-list-rule/number-types/table/delete-number-types-dialog.tsx
+++ b/lib/docu-list-rule/number-types/table/delete-number-types-dialog.tsx
@@ -27,7 +27,7 @@ import {
DrawerTrigger,
} from "@/components/ui/drawer"
-import { deleteNumberType } from "../service"
+import { deleteNumberType } from "@/lib/docu-list-rule/number-types/service"
import { documentNumberTypes } from "@/db/schema/docu-list-rule"
interface DeleteNumberTypesDialogProps
@@ -74,7 +74,7 @@ export function DeleteNumberTypesDialog({
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<Trash className="mr-2 size-4" aria-hidden="true" />
- 삭제 ({numberTypes.length})
+ delete ({numberTypes.length})
</Button>
</DialogTrigger>
) : null}
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 78c3ec37..964a6bd0 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
@@ -16,7 +16,7 @@ import {
DialogTrigger,
} from "@/components/ui/dialog"
import { toast } from "sonner"
-import { createNumberType } from "../service"
+import { createNumberType } from "@/lib/docu-list-rule/number-types/service"
interface NumberTypeAddDialogProps {
onSuccess: () => void
diff --git a/lib/docu-list-rule/number-types/table/number-type-edit-sheet.tsx b/lib/docu-list-rule/number-types/table/number-type-edit-sheet.tsx
index f4066cea..d7daf149 100644
--- a/lib/docu-list-rule/number-types/table/number-type-edit-sheet.tsx
+++ b/lib/docu-list-rule/number-types/table/number-type-edit-sheet.tsx
@@ -26,7 +26,7 @@ import {
SheetTitle,
} from "@/components/ui/sheet"
import { Input } from "@/components/ui/input"
-import { updateNumberType } from "../service"
+import { updateNumberType } from "@/lib/docu-list-rule/number-types/service"
import { documentNumberTypes } from "@/db/schema/docu-list-rule"
const updateNumberTypeSchema = z.object({
diff --git a/lib/docu-list-rule/number-types/table/number-types-table-toolbar.tsx b/lib/docu-list-rule/number-types/table/number-types-table-toolbar.tsx
index 306a3d74..28afe63b 100644
--- a/lib/docu-list-rule/number-types/table/number-types-table-toolbar.tsx
+++ b/lib/docu-list-rule/number-types/table/number-types-table-toolbar.tsx
@@ -6,8 +6,8 @@ import { Download } from "lucide-react"
import { exportTableToExcel } from "@/lib/export"
import { Button } from "@/components/ui/button"
-import { DeleteNumberTypesDialog } from "./delete-number-types-dialog"
-import { NumberTypeAddDialog } from "./number-type-add-dialog"
+import { DeleteNumberTypesDialog } from "@/lib/docu-list-rule/number-types/table/delete-number-types-dialog"
+import { NumberTypeAddDialog } from "@/lib/docu-list-rule/number-types/table/number-type-add-dialog"
import { documentNumberTypes } from "@/db/schema/docu-list-rule"
interface NumberTypesTableToolbarActionsProps {
@@ -33,21 +33,6 @@ export function NumberTypesTableToolbarActions({ table, onSuccess }: NumberTypes
<NumberTypeAddDialog onSuccess={onSuccess || (() => {})} />
- {/** 3) Export 버튼 */}
- <Button
- variant="outline"
- size="sm"
- onClick={() =>
- exportTableToExcel(table, {
- filename: "number-types-list",
- excludeColumns: ["select", "actions"],
- })
- }
- className="gap-2"
- >
- <Download className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">Export</span>
- </Button>
</div>
)
} \ No newline at end of file
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 00f7e2ab..18aa1a30 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
@@ -6,12 +6,11 @@ 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, DataTableRowAction } from "@/types/table"
-import { getNumberTypes } from "../service"
-import { getColumns } from "./number-types-table-columns"
-import { NumberTypeEditSheet } from "./number-type-edit-sheet"
-import { DeleteNumberTypesDialog } from "./delete-number-types-dialog"
-import { NumberTypesTableToolbarActions } from "./number-types-table-toolbar"
+import { getColumns } from "@/lib/docu-list-rule/number-types/table/number-types-table-columns"
+import { DeleteNumberTypesDialog } from "@/lib/docu-list-rule/number-types/table/delete-number-types-dialog"
+import { NumberTypesTableToolbarActions } from "@/lib/docu-list-rule/number-types/table/number-types-table-toolbar"
import { documentNumberTypes } from "@/db/schema/docu-list-rule"
+import { NumberTypeEditSheet } from "@/lib/docu-list-rule/number-types/table/number-type-edit-sheet"
interface NumberTypesTableProps {
promises?: Promise<[{ data: typeof documentNumberTypes.$inferSelect[]; pageCount: number }]>
@@ -47,7 +46,6 @@ export function NumberTypesTable({ promises }: NumberTypesTableProps) {
pageCount: rawData[0].pageCount,
enablePinning: true,
enableAdvancedFilter: true,
- manualSorting: false,
initialState: {
sorting: [{ id: "createdAt", desc: true }],
columnPinning: { right: ["actions"] },
diff --git a/lib/docu-list-rule/number-types/validation.ts b/lib/docu-list-rule/number-types/validation.ts
index 72e2921d..6382ee5a 100644
--- a/lib/docu-list-rule/number-types/validation.ts
+++ b/lib/docu-list-rule/number-types/validation.ts
@@ -1,5 +1,5 @@
import { createSearchParamsCache } from "nuqs/server";
-import { parseAsInteger, parseAsString, parseAsArrayOf, parseAsStringEnum } from "nuqs/server";
+import { parseAsInteger, parseAsString, parseAsStringEnum } from "nuqs/server";
import { getSortingStateParser, getFiltersStateParser } from "@/lib/parsers";
export const searchParamsNumberTypesCache = createSearchParamsCache({
diff --git a/lib/docu-list-rule/types.ts b/lib/docu-list-rule/types.ts
index a5277132..cb80316a 100644
--- a/lib/docu-list-rule/types.ts
+++ b/lib/docu-list-rule/types.ts
@@ -4,7 +4,6 @@ export interface NumberTypeConfig {
id: number
documentNumberTypeId: number
codeGroupId: number | null
- documentClassId: number | null
sdq: number
description: string | null
remark: string | null
@@ -13,8 +12,6 @@ export interface NumberTypeConfig {
updatedAt: Date
codeGroupName: string | null
codeGroupControlType: string | null
- documentClassName: string | null
- documentClassDescription: string | null
}
export interface CodeGroup {