diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-11 00:19:38 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-11 00:19:38 +0000 |
| commit | a383fd2a30f60360ebc0c1b897b3d43cbae178fa (patch) | |
| tree | 98adaae908465697ad627391e66113dc0694abb3 | |
| parent | 71f4e15800b0cf771d1dddab6cc46fc7c2a17c51 (diff) | |
| parent | f30678aa2956620177e61cbcc0fb57e9ce6872f1 (diff) | |
Merge branch 'dujinkim' of https://github.com/DTS-Development/SHI_EVCP into dujinkim
17 files changed, 511 insertions, 115 deletions
diff --git a/app/[lng]/evcp/(evcp)/docu-list-rule/number-types/page.tsx b/app/[lng]/evcp/(evcp)/docu-list-rule/number-types/page.tsx index 6fa010c7..58af176f 100644 --- a/app/[lng]/evcp/(evcp)/docu-list-rule/number-types/page.tsx +++ b/app/[lng]/evcp/(evcp)/docu-list-rule/number-types/page.tsx @@ -3,7 +3,7 @@ import { Shell } from "@/components/shell"; import { Skeleton } from "@/components/ui/skeleton"; import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; import { NumberTypesTable } from "@/lib/docu-list-rule/number-types/table/number-types-table"; -import { getNumberTypes } from "@/lib/docu-list-rule/number-types/service"; +import { getNumberTypesWithConfigs } from "@/lib/docu-list-rule/number-types/service"; import { InformationButton } from "@/components/information/information-button"; import { searchParamsNumberTypesCache } from "@/lib/docu-list-rule/number-types/validation"; @@ -15,7 +15,7 @@ export default async function IndexPage(props: IndexPageProps) { const searchParams = await props.searchParams; const promises = Promise.all([ - getNumberTypes( + getNumberTypesWithConfigs( searchParamsNumberTypesCache.parse(searchParams) ), ]); diff --git a/db/schema/docu-list-rule.ts b/db/schema/docu-list-rule.ts index 505b1c0d..27cace80 100644 --- a/db/schema/docu-list-rule.ts +++ b/db/schema/docu-list-rule.ts @@ -110,7 +110,7 @@ export const documentNumberTypes = pgTable("document_number_types", { // ===== Document Number Type Configs 테이블 ===== export const documentNumberTypeConfigs = pgTable("document_number_type_configs", { id: serial("id").primaryKey(), - documentNumberTypeId: integer("document_number_type_id").notNull().references(() => documentNumberTypes.id), + documentNumberTypeId: integer("document_number_type_id").notNull().references(() => documentNumberTypes.id, { onDelete: "cascade" }), codeGroupId: integer("code_group_id").references(() => codeGroups.id), // Code Group 참조 sdq: integer("sdq").notNull(), // 순서 번호 (1, 2, 3, 4, 5, 6) description: varchar("description", { length: 200 }), // Description (예: [001] PROJECT NO) @@ -143,7 +143,6 @@ export const codeGroupsRelations = relations(codeGroups, ({ many, one }) => ({ fields: [codeGroups.projectId], references: [projects.id], }), - documentClasses: many(documentClasses), // Code Group에 속한 Document Classes comboBoxSettings: many(comboBoxSettings), // Code Group에 속한 ComboBox Settings documentNumberTypeConfigs: many(documentNumberTypeConfigs), // Code Group을 참조하는 Number Type Configs })) @@ -155,10 +154,6 @@ export const documentClassesRelations = relations(documentClasses, ({ many, one references: [projects.id], }), documentClassOptions: many(documentClassOptions), // Document Class 하위 옵션들 - codeGroup: one(codeGroups, { - fields: [documentClasses.codeGroupId], - references: [codeGroups.id], - }), })) // Document Class Options 관계 diff --git a/lib/docu-list-rule/code-groups/service.ts b/lib/docu-list-rule/code-groups/service.ts index 843bafa2..c854f6c9 100644 --- a/lib/docu-list-rule/code-groups/service.ts +++ b/lib/docu-list-rule/code-groups/service.ts @@ -2,26 +2,33 @@ import { revalidatePath } from "next/cache" import db from "@/db/db" -import { codeGroups, comboBoxSettings } from "@/db/schema/docu-list-rule" +import { codeGroups, comboBoxSettings, documentNumberTypeConfigs, documentNumberTypes } from "@/db/schema/docu-list-rule" import { projects } from "@/db/schema/projects" -import { eq, sql, count, and } from "drizzle-orm" +import { eq, sql } from "drizzle-orm" import { unstable_noStore } from "next/cache" // Code Groups 목록 조회 -export async function getCodeGroups(input: any) { +export async function getCodeGroups(input: { + page: number + perPage: number + search: string + filters: Array<{ id: string; value: string | string[] }> + joinOperator: "and" | "or" + sort: Array<{ id: string; desc: boolean }> +}) { unstable_noStore() try { const { page, perPage, search, filters, joinOperator } = input const offset = (page - 1) * perPage - // 검색 조건 (Document Class 제외) - let whereConditions = sql`${codeGroups.groupId} != 'DOC_CLASS'` + // 검색 조건 (plant 타입 프로젝트만) + let whereConditions = sql`${projects.type} = 'plant'` // 검색어 필터링 if (search) { const searchTerm = `%${search}%` - whereConditions = sql`${codeGroups.groupId} != 'DOC_CLASS' AND ( + whereConditions = sql`${projects.type} = 'plant' AND ( ${codeGroups.groupId} ILIKE ${searchTerm} OR ${codeGroups.description} ILIKE ${searchTerm} OR ${codeGroups.codeFormat} ILIKE ${searchTerm} OR @@ -32,7 +39,7 @@ export async function getCodeGroups(input: any) { // 고급 필터링 (단순화) if (filters && filters.length > 0) { - const filterConditions = filters.map(filter => { + const filterConditions = filters.map((filter: { id: string; value: string | string[] }) => { const { id, value } = filter if (!value || Array.isArray(value)) return null @@ -56,7 +63,7 @@ export async function getCodeGroups(input: any) { if (filterConditions.length > 0) { const operator = joinOperator === "or" ? sql` OR ` : sql` AND ` - const combinedFilters = filterConditions.reduce((acc, condition, index) => { + const combinedFilters = filterConditions.reduce((acc: ReturnType<typeof sql> | null, condition: ReturnType<typeof sql> | null, index: number) => { if (index === 0) return condition return sql`${acc}${operator}${condition}` }) @@ -98,6 +105,7 @@ export async function getCodeGroups(input: any) { projectId: codeGroups.projectId, projectCode: projects.code, projectName: projects.name, + projectType: projects.type, }) .from(codeGroups) .leftJoin(projects, eq(codeGroups.projectId, projects.id)) @@ -140,14 +148,11 @@ export async function createCodeGroup(input: { isActive?: boolean }) { try { - // 해당 프로젝트의 마지막 Code Group의 groupId를 찾아서 다음 번호 생성 (DOC_CLASS 제외) + // 해당 프로젝트의 마지막 Code Group의 groupId를 찾아서 다음 번호 생성 const lastCodeGroup = await db .select({ groupId: codeGroups.groupId }) .from(codeGroups) - .where(and( - eq(codeGroups.projectId, input.projectId), // projectId로 변경 - sql`${codeGroups.groupId} != 'DOC_CLASS'` - )) + .where(eq(codeGroups.projectId, input.projectId)) // projectId로 변경 .orderBy(sql`CAST(SUBSTRING(${codeGroups.groupId}, 6) AS INTEGER) DESC`) .limit(1) @@ -251,6 +256,28 @@ export async function deleteCodeGroup(id: number) { } } + // Number Type Config에서 사용 중인지 확인 (Number Type 정보 포함) + const numberTypeConfigs = await db + .select({ + id: documentNumberTypeConfigs.id, + numberTypeName: documentNumberTypes.name, + numberTypeDescription: documentNumberTypes.description + }) + .from(documentNumberTypeConfigs) + .leftJoin(documentNumberTypes, eq(documentNumberTypeConfigs.documentNumberTypeId, documentNumberTypes.id)) + .where(eq(documentNumberTypeConfigs.codeGroupId, id)) + + if (numberTypeConfigs.length > 0) { + const numberTypeNames = numberTypeConfigs.map(config => + `${config.numberTypeName}${config.numberTypeDescription ? ` (${config.numberTypeDescription})` : ''}` + ).join(', ') + + return { + success: false, + error: `Code Group "${codeGroup[0].description}"은(는) ${numberTypeNames}에서 사용 중이므로 삭제할 수 없습니다. Number Type별 설정에서 먼저 제거해주세요.` + } + } + // Control Type이 combobox인 경우 관련 Combo Box 옵션들도 삭제 if (codeGroup[0].controlType === 'combobox') { // Combo Box 옵션들 삭제 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 a0143239..33dfdd03 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 @@ -56,7 +56,7 @@ interface CodeGroupsAddDialogProps { export function CodeGroupsAddDialog({ onSuccess }: CodeGroupsAddDialogProps) { const [open, setOpen] = React.useState(false) const [isLoading, setIsLoading] = React.useState(false) - const [projects, setProjects] = React.useState<Array<{ id: number; code: string; name: string }>>([]) + const [projects, setProjects] = React.useState<Array<{ id: number; code: string; name: string; type: string }>>([]) const form = useForm<CreateCodeGroupFormValues>({ resolver: zodResolver(createCodeGroupSchema), @@ -80,14 +80,20 @@ export function CodeGroupsAddDialog({ onSuccess }: CodeGroupsAddDialogProps) { sort: [], filters: [], joinOperator: "and", - flags: [] + flags: [], + code: "", + name: "", + type: "" }) if (result.data) { - setProjects(result.data) + // plant 타입의 프로젝트만 필터링 + const plantProjects = result.data.filter(project => project.type === 'plant') + setProjects(plantProjects) } } catch (error) { console.error("Failed to load projects:", error) toast.error("프로젝트 목록을 불러오는데 실패했습니다.") + } } loadProjects() diff --git a/lib/docu-list-rule/combo-box-settings/service.ts b/lib/docu-list-rule/combo-box-settings/service.ts index 7a003327..c733f978 100644 --- a/lib/docu-list-rule/combo-box-settings/service.ts +++ b/lib/docu-list-rule/combo-box-settings/service.ts @@ -26,13 +26,13 @@ export async function getComboBoxCodeGroups(input: { const { page, perPage, sort, search, filters, joinOperator } = input const offset = (page - 1) * perPage - // Control Type이 combobox인 조건 - let whereConditions = sql`${codeGroups.controlType} = 'combobox'` + // Control Type이 combobox이고 plant 타입 프로젝트인 조건 + let whereConditions = sql`${codeGroups.controlType} = 'combobox' AND ${projects.type} = 'plant'` // 검색 조건 if (search) { const searchTerm = `%${search}%` - whereConditions = sql`${whereConditions} AND ( + whereConditions = sql`${codeGroups.controlType} = 'combobox' AND ${projects.type} = 'plant' AND ( ${codeGroups.groupId} ILIKE ${searchTerm} OR ${codeGroups.description} ILIKE ${searchTerm} OR ${codeGroups.codeFormat} ILIKE ${searchTerm} OR diff --git a/lib/docu-list-rule/document-class/service.ts b/lib/docu-list-rule/document-class/service.ts index 91a4e053..2ec31ae6 100644 --- a/lib/docu-list-rule/document-class/service.ts +++ b/lib/docu-list-rule/document-class/service.ts @@ -23,13 +23,13 @@ export async function getDocumentClassCodeGroups(input: { const { page, perPage, sort, search, filters, joinOperator } = input const offset = (page - 1) * perPage - // 기본 조건 - let whereConditions = sql`${documentClasses.isActive} = true` + // 기본 조건 (plant 타입 프로젝트만) + let whereConditions = sql`${documentClasses.isActive} = true AND ${projects.type} = 'plant'` // 검색 조건 if (search) { const searchTerm = `%${search}%` - whereConditions = sql`${whereConditions} AND ( + whereConditions = sql`${documentClasses.isActive} = true AND ${projects.type} = 'plant' AND ( ${documentClasses.code} ILIKE ${searchTerm} OR ${documentClasses.value} ILIKE ${searchTerm} OR ${documentClasses.description} ILIKE ${searchTerm} OR @@ -102,7 +102,8 @@ export async function getDocumentClassCodeGroups(input: { updatedAt: documentClasses.updatedAt, projectId: documentClasses.projectId, projectCode: projects.code, - projectName: projects.name, + projectName: projects.name, + projectType: projects.type, }) .from(documentClasses) .leftJoin(projects, eq(documentClasses.projectId, projects.id)) @@ -179,38 +180,6 @@ export async function createDocumentClassCodeGroup(input: { } } - // 해당 프로젝트의 Code Group이 존재하는지 확인 - const existingCodeGroup = await db - .select({ id: codeGroups.id }) - .from(codeGroups) - .where(and( - eq(codeGroups.projectId, input.projectId), // projectId로 변경 - eq(codeGroups.groupId, 'DOC_CLASS') - )) - .limit(1) - - let codeGroupId: number | null = null - - if (existingCodeGroup.length === 0) { - // Code Group이 없으면 자동으로 생성 - const [newCodeGroup] = await db - .insert(codeGroups) - .values({ - projectId: input.projectId, // projectId로 변경 - groupId: 'DOC_CLASS', - description: 'Document Class', - codeFormat: 'DOC_CLASS_###', - expressions: '^DOC_CLASS_\\d{3}$', - controlType: 'Combobox', - isActive: true, - }) - .returning({ id: codeGroups.id }) - - codeGroupId = newCodeGroup.id - } else { - codeGroupId = existingCodeGroup[0].id - } - const [newDocumentClass] = await db .insert(documentClasses) .values({ @@ -218,7 +187,7 @@ export async function createDocumentClassCodeGroup(input: { code: newCode, value: formattedValue, description: input.description || "", - codeGroupId: codeGroupId, + codeGroupId: null, // Code Group 연결 제거 isActive: true, }) .returning({ id: documentClasses.id }) diff --git a/lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx b/lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx index dfd1d7f2..a51b0598 100644 --- a/lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx +++ b/lib/docu-list-rule/document-class/table/document-class-add-dialog.tsx @@ -54,7 +54,7 @@ export function DocumentClassAddDialog({ }: DocumentClassAddDialogProps) { const [open, setOpen] = React.useState(false) const [isPending, startTransition] = React.useTransition() - const [projects, setProjects] = React.useState<Array<{ id: number; code: string; name: string }>>([]) + const [projects, setProjects] = React.useState<Array<{ id: number; code: string; name: string; type: string }>>([]) const form = useForm<CreateDocumentClassSchema>({ resolver: zodResolver(createDocumentClassSchema), @@ -78,10 +78,15 @@ export function DocumentClassAddDialog({ sort: [], filters: [], joinOperator: "and", - flags: [] + flags: [], + code: "", + name: "", + type: "" }) if (result.data) { - setProjects(result.data) + // plant 타입의 프로젝트만 필터링 + const plantProjects = result.data.filter(project => project.type === 'plant') + setProjects(plantProjects) } } catch (error) { console.error("Failed to load projects:", error) 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 c9156ff7..03855fe1 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 @@ -25,7 +25,6 @@ export function DocumentClassTable({ promises }: DocumentClassTableProps) { const [selectedDocumentClass, setSelectedDocumentClass] = React.useState<typeof documentClasses.$inferSelect | null>(null) const refreshData = React.useCallback(() => { - // 전체 페이지 새로고침 대신 router.refresh() 사용 (성능 개선) router.refresh() }, [router]) diff --git a/lib/docu-list-rule/number-type-configs/service.ts b/lib/docu-list-rule/number-type-configs/service.ts index 14cfc2f0..01e045ed 100644 --- a/lib/docu-list-rule/number-type-configs/service.ts +++ b/lib/docu-list-rule/number-type-configs/service.ts @@ -3,7 +3,7 @@ import { revalidatePath } from "next/cache" import db from "@/db/db" import { unstable_noStore } from "next/cache" -import { documentNumberTypeConfigs, codeGroups, documentNumberTypes } from "@/db/schema/docu-list-rule" +import { documentNumberTypeConfigs, codeGroups } from "@/db/schema/docu-list-rule" import { projects } from "@/db/schema/projects" import { asc, eq, sql, and } from "drizzle-orm" import { GetNumberTypeConfigsSchema } from "./validation" @@ -277,6 +277,92 @@ export async function updateNumberTypeConfig(input: { } } +// Number Type Config 순서 변경 (간단한 방식) +export async function updateNumberTypeConfigOrder(input: { + id: number + sdq: number +}) { + try { + // 현재 수정 중인 항목의 documentNumberTypeId 가져오기 + const currentConfig = await db + .select({ + documentNumberTypeId: documentNumberTypeConfigs.documentNumberTypeId, + currentSdq: documentNumberTypeConfigs.sdq + }) + .from(documentNumberTypeConfigs) + .where(eq(documentNumberTypeConfigs.id, input.id)) + .limit(1) + + if (currentConfig.length === 0) { + return { + success: false, + error: "Config not found" + } + } + + const { documentNumberTypeId, currentSdq } = currentConfig[0] + + // 임시 값으로 먼저 업데이트 (중복 방지) + const tempSdq = -999999 // 충분히 작은 임시 값 + await db + .update(documentNumberTypeConfigs) + .set({ + sdq: tempSdq, + updatedAt: new Date(), + }) + .where(eq(documentNumberTypeConfigs.id, input.id)) + + // 다른 항목들의 순서 조정 + if (input.sdq > currentSdq) { + // 순서가 증가한 경우: 현재 순서와 새 순서 사이의 항목들을 1씩 감소 + await db + .update(documentNumberTypeConfigs) + .set({ + sdq: sql`${documentNumberTypeConfigs.sdq} - 1`, + updatedAt: new Date(), + }) + .where( + sql`${documentNumberTypeConfigs.documentNumberTypeId} = ${documentNumberTypeId} AND ${documentNumberTypeConfigs.sdq} > ${currentSdq} AND ${documentNumberTypeConfigs.sdq} <= ${input.sdq} AND ${documentNumberTypeConfigs.id} != ${input.id}` + ) + } else if (input.sdq < currentSdq) { + // 순서가 감소한 경우: 새 순서와 현재 순서 사이의 항목들을 1씩 증가 + await db + .update(documentNumberTypeConfigs) + .set({ + sdq: sql`${documentNumberTypeConfigs.sdq} + 1`, + updatedAt: new Date(), + }) + .where( + sql`${documentNumberTypeConfigs.documentNumberTypeId} = ${documentNumberTypeId} AND ${documentNumberTypeConfigs.sdq} >= ${input.sdq} AND ${documentNumberTypeConfigs.sdq} < ${currentSdq} AND ${documentNumberTypeConfigs.id} != ${input.id}` + ) + } + + // 최종 순서로 업데이트 + const [result] = await db + .update(documentNumberTypeConfigs) + .set({ + sdq: input.sdq, + updatedAt: new Date(), + }) + .where(eq(documentNumberTypeConfigs.id, input.id)) + .returning({ id: documentNumberTypeConfigs.id }) + + revalidatePath("/evcp/docu-list-rule/number-type-configs") + + return { + success: true, + data: result, + message: "Number Type Config order updated successfully" + } + } catch (error) { + console.error("Error updating number type config order:", error) + return { + success: false, + error: "Failed to update number type config order" + } + } +} + // Number Type Config 삭제 export async function deleteNumberTypeConfig(id: number) { try { @@ -342,7 +428,7 @@ export async function getActiveCodeGroups(projectId?: number) { // 프로젝트별 필터링 추가 if (projectId) { - whereConditions = and(whereConditions, eq(codeGroups.projectId, projectId)) + whereConditions = and(whereConditions, eq(codeGroups.projectId, projectId)) || whereConditions } const codeGroupsData = await db 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 a6ba3e50..1bddfa52 100644 --- a/lib/docu-list-rule/number-type-configs/table/number-type-configs-table.tsx +++ b/lib/docu-list-rule/number-type-configs/table/number-type-configs-table.tsx @@ -8,7 +8,7 @@ import { arrayMove } from '@dnd-kit/sortable' import { DragEndEvent } from '@dnd-kit/core' import { toast } from "sonner" -import { getNumberTypeConfigs, updateNumberTypeConfig } from "@/lib/docu-list-rule/number-type-configs/service" +import { getNumberTypeConfigs, updateNumberTypeConfig, updateNumberTypeConfigOrder } from "@/lib/docu-list-rule/number-type-configs/service" import { getColumns } from "@/lib/docu-list-rule/number-type-configs/table/number-type-configs-table-columns" import { DeleteNumberTypeConfigsDialog } from "@/lib/docu-list-rule/number-type-configs/table/delete-number-type-configs-dialog" import { NumberTypeConfigsEditDialog } from "@/lib/docu-list-rule/number-type-configs/table/number-type-configs-edit-dialog" @@ -109,7 +109,7 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon } }, [rawData, fetchConfigs]) - // Number Type 변경 핸들러 (서버 사이드 처리 지원) + const handleNumberTypeChange = React.useCallback((numberTypeId: number) => { setSelectedNumberType(numberTypeId) @@ -141,18 +141,31 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon setConfigsData(prev => ({ ...prev, data: updatedConfigsData })) - // 서버에 순서 업데이트 + // 서버에 순서 업데이트 (일괄 처리) try { + // 모든 항목을 임시 값으로 먼저 업데이트 + for (let i = 0; i < updatedConfigsData.length; i++) { + const config = updatedConfigsData[i] + await updateNumberTypeConfig({ + id: config.id, + codeGroupId: config.codeGroupId, + sdq: -(i + 1), // 임시 음수 값 + description: config.description || undefined, + remark: config.remark || undefined, + }) + } + + // 최종 순서로 업데이트 for (const config of updatedConfigsData) { await updateNumberTypeConfig({ id: config.id, codeGroupId: config.codeGroupId, - sdq: config.sdq, description: config.description || undefined, remark: config.remark || undefined, }) } + toast.success("순서가 성공적으로 변경되었습니다.") } catch (error) { console.error("Error updating order:", error) @@ -209,14 +222,9 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon if (selectedNumberType) { const state = table.getState() const currentSearchTerm = state.globalFilter || "" - - console.log("🔍 useEffect triggered - currentSearchTerm:", `"${currentSearchTerm}"`, "lastSearchTerm:", `"${lastSearchTerm}"`) - - // 검색어가 실제로 변경되었을 때만 API 호출 + // 검색어가 실제로 변경되었을 때만 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, @@ -224,19 +232,16 @@ export function NumberTypeConfigsTable({ promises, searchParams }: NumberTypeCon 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("") } }, [searchParams.search, table]) diff --git a/lib/docu-list-rule/number-type-configs/table/number-type-selector.tsx b/lib/docu-list-rule/number-type-configs/table/number-type-selector.tsx index fe8d0895..d01e25d5 100644 --- a/lib/docu-list-rule/number-type-configs/table/number-type-selector.tsx +++ b/lib/docu-list-rule/number-type-configs/table/number-type-selector.tsx @@ -126,7 +126,7 @@ export function NumberTypeSelector({ <SelectContent> {projectGroups.map((project) => ( <SelectItem key={project.id} value={project.id.toString()}> - {project.code} - {project.name} ({project.numberTypes.length}개) + {project.code} - {project.name} </SelectItem> ))} </SelectContent> diff --git a/lib/docu-list-rule/number-types/service.ts b/lib/docu-list-rule/number-types/service.ts index a7c32274..18c5e6a3 100644 --- a/lib/docu-list-rule/number-types/service.ts +++ b/lib/docu-list-rule/number-types/service.ts @@ -2,7 +2,7 @@ import { revalidatePath } from "next/cache" import db from "@/db/db" -import { documentNumberTypes, documentNumberTypeConfigs } from "@/db/schema/docu-list-rule" +import { documentNumberTypes, documentNumberTypeConfigs, codeGroups } from "@/db/schema/docu-list-rule" import { projects } from "@/db/schema/projects" import { eq, sql, and } from "drizzle-orm" import { unstable_noStore } from "next/cache" @@ -26,13 +26,13 @@ export async function getNumberTypes(input: { const { page, perPage, sort, search, filters, joinOperator } = input const offset = (page - 1) * perPage - // 기본 조건 - let whereConditions = sql`1=1` + // 기본 조건 (plant 타입 프로젝트만) + let whereConditions = sql`${projects.type} = 'plant'` // 검색 조건 if (search) { const searchTerm = `%${search}%` - whereConditions = sql`${whereConditions} AND ( + whereConditions = sql`${projects.type} = 'plant' AND ( ${documentNumberTypes.name} ILIKE ${searchTerm} OR ${documentNumberTypes.description} ILIKE ${searchTerm} OR ${projects.code} ILIKE ${searchTerm} @@ -242,43 +242,274 @@ export async function updateNumberType(input: { // Number Type 삭제 export async function deleteNumberType(id: number) { try { - // 관련된 config가 있는지 확인 - const relatedConfigs = await db - .select({ id: documentNumberTypeConfigs.id }) - .from(documentNumberTypeConfigs) - .where(eq(documentNumberTypeConfigs.documentNumberTypeId, id)) + // Number Type 정보 조회 + const numberType = await db + .select({ + id: documentNumberTypes.id, + name: documentNumberTypes.name, + description: documentNumberTypes.description + }) + .from(documentNumberTypes) + .where(eq(documentNumberTypes.id, id)) .limit(1) - if (relatedConfigs.length > 0) { + if (numberType.length === 0) { return { success: false, - error: "Cannot delete Number Type with existing configurations" + error: "Number Type not found" } } - const [deletedNumberType] = await db + // 관련된 config가 있는지 확인 (Code Group 정보 포함) + const relatedConfigs = await db + .select({ + id: documentNumberTypeConfigs.id, + codeGroupDescription: codeGroups.description + }) + .from(documentNumberTypeConfigs) + .leftJoin(codeGroups, eq(documentNumberTypeConfigs.codeGroupId, codeGroups.id)) + .where(eq(documentNumberTypeConfigs.documentNumberTypeId, id)) + + // 설정 정보를 반환 (삭제는 허용하되 경고용) + const configInfo = relatedConfigs.length > 0 ? { + hasConfigs: true, + codeGroupNames: relatedConfigs.map(config => + config.codeGroupDescription || "Unknown Code Group" + ) + } : { + hasConfigs: false, + codeGroupNames: [] + } + + // Number Type 삭제 (CASCADE로 관련 config들도 자동 삭제됨) + await db .delete(documentNumberTypes) .where(eq(documentNumberTypes.id, id)) - .returning({ id: documentNumberTypes.id }) - if (!deletedNumberType) { + revalidatePath("/evcp/docu-list-rule/number-types") + + return { + success: true, + message: configInfo.hasConfigs + ? `Number Type "${numberType[0].name}"이(가) 설정과 함께 삭제되었습니다.` + : "Number Type deleted successfully", + configInfo + } + } catch (error) { + console.error("Error deleting number type:", error) + return { + success: false, + error: "Failed to delete number type" + } + } +} + +// Number Type 설정 정보 확인 (삭제하지 않고 정보만 반환) +export async function checkNumberTypeConfigs(id: number) { + try { + // Number Type 정보 조회 + const numberType = await db + .select({ + id: documentNumberTypes.id, + name: documentNumberTypes.name, + description: documentNumberTypes.description + }) + .from(documentNumberTypes) + .where(eq(documentNumberTypes.id, id)) + .limit(1) + + if (numberType.length === 0) { return { success: false, error: "Number Type not found" } } - revalidatePath("/evcp/docu-list-rule/number-types") - + // 관련된 config가 있는지 확인 (Code Group 정보 포함) + const relatedConfigs = await db + .select({ + id: documentNumberTypeConfigs.id, + codeGroupDescription: codeGroups.description + }) + .from(documentNumberTypeConfigs) + .leftJoin(codeGroups, eq(documentNumberTypeConfigs.codeGroupId, codeGroups.id)) + .where(eq(documentNumberTypeConfigs.documentNumberTypeId, id)) + + const configInfo = relatedConfigs.length > 0 ? { + hasConfigs: true, + codeGroupNames: relatedConfigs.map(config => + config.codeGroupDescription || "Unknown Code Group" + ) + } : { + hasConfigs: false, + codeGroupNames: [] + } + return { success: true, - message: "Number Type deleted successfully" + data: { + numberType: numberType[0], + configInfo + } } } catch (error) { - console.error("Error deleting number type:", error) + console.error("Error checking number type configs:", error) return { success: false, - error: "Failed to delete number type" + error: "Failed to check number type configs" + } + } +} + +// Number Types 목록 조회 (설정 정보 포함) +export async function getNumberTypesWithConfigs(input: { + page: number + perPage: number + search?: string + sort?: Array<{ id: string; desc: boolean }> + filters?: Array<{ id: string; value: string }> + joinOperator?: "and" | "or" + flags?: string[] + numberTypeId?: string + description?: string + isActive?: string +}) { + unstable_noStore() + + try { + const { page, perPage, sort, search, filters, joinOperator } = input + const offset = (page - 1) * perPage + + // 기본 조건 (plant 타입 프로젝트만) + let whereConditions = sql`${projects.type} = 'plant'` + + // 검색 조건 + if (search) { + const searchTerm = `%${search}%` + whereConditions = sql`${projects.type} = 'plant' AND ( + ${documentNumberTypes.name} ILIKE ${searchTerm} OR + ${documentNumberTypes.description} ILIKE ${searchTerm} OR + ${projects.code} ILIKE ${searchTerm} + )` + } + + // 고급 필터링 + if (filters && filters.length > 0) { + const filterConditions = filters.map(filter => { + const { id, value } = filter + if (!value) return null + + switch (id) { + case "name": + return sql`${documentNumberTypes.name} ILIKE ${`%${value}%`}` + case "description": + return sql`${documentNumberTypes.description} ILIKE ${`%${value}%`}` + case "isActive": + return sql`${documentNumberTypes.isActive} = ${value === "true"}` + case "createdAt": + return sql`${documentNumberTypes.createdAt}::text ILIKE ${`%${value}%`}` + default: + return null + } + }).filter(Boolean) + + if (filterConditions.length > 0) { + const operator = joinOperator === "or" ? sql` OR ` : sql` AND ` + const combinedFilters = filterConditions.reduce((acc, condition, index) => { + if (index === 0) return condition + return sql`${acc}${operator}${condition}` + }) + + whereConditions = sql`${whereConditions} AND (${combinedFilters})` + } + } + + // 정렬 (안전한 필드 체크 적용) + let orderBy = sql`${documentNumberTypes.name} ASC` + if (sort && sort.length > 0) { + const sortField = sort[0] + // 안전성 체크: 필드가 실제 테이블에 존재하는지 확인 + if (sortField && sortField.id && typeof sortField.id === "string") { + const direction = sortField.desc ? sql`DESC` : sql`ASC` + + // 프로젝트 코드 정렬 처리 + if (sortField.id === "projectCode") { + orderBy = sql`${projects.code} ${direction}` + } else if (sortField.id in documentNumberTypes) { + const col = documentNumberTypes[sortField.id as keyof typeof documentNumberTypes] + orderBy = sql`${col} ${direction}` + } + } + } + + // 데이터 조회 (프로젝트 정보 포함) + const data = await db + .select({ + id: documentNumberTypes.id, + name: documentNumberTypes.name, + description: documentNumberTypes.description, + isActive: documentNumberTypes.isActive, + createdAt: documentNumberTypes.createdAt, + updatedAt: documentNumberTypes.updatedAt, + projectId: documentNumberTypes.projectId, + projectCode: projects.code, + projectName: projects.name, + projectType: projects.type, + }) + .from(documentNumberTypes) + .leftJoin(projects, eq(documentNumberTypes.projectId, projects.id)) + .where(whereConditions) + .orderBy(orderBy) + .limit(perPage) + .offset(offset) + + // 각 Number Type에 대한 설정 정보 조회 + const dataWithConfigs = await Promise.all( + data.map(async (numberType) => { + const relatedConfigs = await db + .select({ + id: documentNumberTypeConfigs.id, + codeGroupDescription: codeGroups.description + }) + .from(documentNumberTypeConfigs) + .leftJoin(codeGroups, eq(documentNumberTypeConfigs.codeGroupId, codeGroups.id)) + .where(eq(documentNumberTypeConfigs.documentNumberTypeId, numberType.id)) + + const configInfo = relatedConfigs.length > 0 ? { + hasConfigs: true, + codeGroupNames: relatedConfigs.map(config => + config.codeGroupDescription || "Unknown Code Group" + ) + } : { + hasConfigs: false, + codeGroupNames: [] + } + + return { + ...numberType, + configInfo + } + }) + ) + + // 전체 개수 조회 + const totalCount = await db + .select({ count: sql<number>`count(*)` }) + .from(documentNumberTypes) + .leftJoin(projects, eq(documentNumberTypes.projectId, projects.id)) + .where(whereConditions) + + return { + data: dataWithConfigs, + totalCount: totalCount[0].count, + pageCount: Math.ceil(totalCount[0].count / perPage), + } + } catch (error) { + console.error("Error fetching number types with configs:", error) + return { + data: [], + totalCount: 0, + pageCount: 0, } } }
\ No newline at end of file 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 f0d80350..90ac949e 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 @@ -30,9 +30,17 @@ import { import { deleteNumberType } from "@/lib/docu-list-rule/number-types/service" import { documentNumberTypes } from "@/db/schema/docu-list-rule" +// Number Type with configs 타입 정의 +type NumberTypeWithConfigs = typeof documentNumberTypes.$inferSelect & { + configInfo: { + hasConfigs: boolean + codeGroupNames: string[] + } +} + interface DeleteNumberTypesDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> { - numberTypes: typeof documentNumberTypes.$inferSelect[] + numberTypes: NumberTypeWithConfigs[] showTrigger?: boolean onSuccess?: () => void } @@ -46,6 +54,9 @@ export function DeleteNumberTypesDialog({ const [isDeletePending, startDeleteTransition] = React.useTransition() const isDesktop = useMediaQuery("(min-width: 640px)") + // 설정이 있는 Number Types 필터링 + const numberTypesWithConfigs = numberTypes.filter(numberType => numberType.configInfo.hasConfigs) + function onDelete() { startDeleteTransition(async () => { try { @@ -85,6 +96,24 @@ export function DeleteNumberTypesDialog({ 이 작업은 되돌릴 수 없습니다. 선택된{" "} <span className="font-medium">{numberTypes.length}</span> 개의 Number Type을 서버에서 영구적으로 삭제합니다. + {numberTypesWithConfigs.length > 0 && ( + <div className="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg"> + <div className="text-sm font-medium text-yellow-800 mb-2"> + Number Type별 설정 삭제 경고 + </div> + <div className="text-sm text-yellow-700"> + 다음 {numberTypesWithConfigs.length}개의 Number Type에 설정이 포함되어 있습니다: + <ul className="mt-1 ml-4 list-disc"> + {numberTypesWithConfigs.map((numberType, index) => ( + <li key={index}> + {numberType.name} ({numberType.configInfo.codeGroupNames.join(', ')}) + </li> + ))} + </ul> + 이 Number Type들을 삭제하면 관련된 모든 설정도 함께 삭제됩니다. + </div> + </div> + )} </DialogDescription> </DialogHeader> <DialogFooter className="gap-2 sm:space-x-0"> @@ -128,6 +157,24 @@ export function DeleteNumberTypesDialog({ 이 작업은 되돌릴 수 없습니다. 선택된{" "} <span className="font-medium">{numberTypes.length}</span> 개의 Number Type을 서버에서 영구적으로 삭제합니다. + {numberTypesWithConfigs.length > 0 && ( + <div className="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg"> + <div className="text-sm font-medium text-yellow-800 mb-2"> + Number Type별 설정 삭제 경고 + </div> + <div className="text-sm text-yellow-700"> + 다음 {numberTypesWithConfigs.length}개의 Number Type에 설정이 포함되어 있습니다: + <ul className="mt-1 ml-4 list-disc"> + {numberTypesWithConfigs.map((numberType, index) => ( + <li key={index}> + {numberType.name} ({numberType.configInfo.codeGroupNames.join(', ')}) + </li> + ))} + </ul> + 이 Number Type들을 삭제하면 관련된 모든 설정도 함께 삭제됩니다. + </div> + </div> + )} </DrawerDescription> </DrawerHeader> <DrawerFooter className="gap-2 sm:space-x-0"> 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 c48eb217..903cd5c6 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 @@ -32,7 +32,7 @@ interface NumberTypeAddDialogProps { export function NumberTypeAddDialog({ onSuccess }: NumberTypeAddDialogProps) { const [open, setOpen] = React.useState(false) const [loading, setLoading] = React.useState(false) - const [projects, setProjects] = React.useState<Array<{ id: number; code: string; name: string }>>([]) + const [projects, setProjects] = React.useState<Array<{ id: number; code: string; name: string; type: string }>>([]) const [formData, setFormData] = React.useState({ projectId: "", name: "", @@ -57,7 +57,9 @@ export function NumberTypeAddDialog({ onSuccess }: NumberTypeAddDialogProps) { perPage: 1000 }) if (result.data) { - setProjects(result.data) + // plant 타입의 프로젝트만 필터링 + const plantProjects = result.data.filter(project => project.type === 'plant') + setProjects(plantProjects) } } catch (error) { console.error("Failed to load projects:", error) diff --git a/lib/docu-list-rule/number-types/table/number-types-table-columns.tsx b/lib/docu-list-rule/number-types/table/number-types-table-columns.tsx index e4bfc345..c529bcfb 100644 --- a/lib/docu-list-rule/number-types/table/number-types-table-columns.tsx +++ b/lib/docu-list-rule/number-types/table/number-types-table-columns.tsx @@ -20,18 +20,26 @@ import { import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" import { documentNumberTypes } from "@/db/schema/docu-list-rule" +// Number Type with configs 타입 정의 +type NumberTypeWithConfigs = typeof documentNumberTypes.$inferSelect & { + configInfo: { + hasConfigs: boolean + codeGroupNames: string[] + } +} + interface GetColumnsProps { - setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<typeof documentNumberTypes.$inferSelect> | null>> + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<NumberTypeWithConfigs> | null>> } /** * tanstack table 컬럼 정의 */ -export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof documentNumberTypes.$inferSelect>[] { +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<NumberTypeWithConfigs>[] { // ---------------------------------------------------------------- // 1) select 컬럼 (체크박스) // ---------------------------------------------------------------- - const selectColumn: ColumnDef<typeof documentNumberTypes.$inferSelect> = { + const selectColumn: ColumnDef<NumberTypeWithConfigs> = { id: "select", header: ({ table }) => ( <Checkbox @@ -60,7 +68,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof // ---------------------------------------------------------------- // 2) actions 컬럼 (Dropdown 메뉴) // ---------------------------------------------------------------- - const actionsColumn: ColumnDef<typeof documentNumberTypes.$inferSelect> = { + const actionsColumn: ColumnDef<NumberTypeWithConfigs> = { id: "actions", enableHiding: false, cell: function Cell({ row }) { @@ -99,7 +107,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<typeof // ---------------------------------------------------------------- // 3) 데이터 컬럼들 // ---------------------------------------------------------------- - const dataColumns: ColumnDef<typeof documentNumberTypes.$inferSelect>[] = [ + const dataColumns: ColumnDef<NumberTypeWithConfigs>[] = [ { accessorKey: "projectCode", enableResizing: true, 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 28afe63b..3d2d9312 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 @@ -10,8 +10,16 @@ import { DeleteNumberTypesDialog } from "@/lib/docu-list-rule/number-types/table import { NumberTypeAddDialog } from "@/lib/docu-list-rule/number-types/table/number-type-add-dialog" import { documentNumberTypes } from "@/db/schema/docu-list-rule" +// Number Type with configs 타입 정의 +type NumberTypeWithConfigs = typeof documentNumberTypes.$inferSelect & { + configInfo: { + hasConfigs: boolean + codeGroupNames: string[] + } +} + interface NumberTypesTableToolbarActionsProps { - table: Table<typeof documentNumberTypes.$inferSelect> + table: Table<NumberTypeWithConfigs> onSuccess?: () => void } 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 66f789d1..22e63363 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 @@ -12,8 +12,16 @@ import { NumberTypesTableToolbarActions } from "@/lib/docu-list-rule/number-type import { documentNumberTypes } from "@/db/schema/docu-list-rule" import { NumberTypeEditSheet } from "@/lib/docu-list-rule/number-types/table/number-type-edit-sheet" +// Number Type with configs 타입 정의 +type NumberTypeWithConfigs = typeof documentNumberTypes.$inferSelect & { + configInfo: { + hasConfigs: boolean + codeGroupNames: string[] + } +} + interface NumberTypesTableProps { - promises?: Promise<[{ data: typeof documentNumberTypes.$inferSelect[]; pageCount: number }]> + promises?: Promise<[{ data: NumberTypeWithConfigs[]; pageCount: number }]> } export function NumberTypesTable({ promises }: NumberTypesTableProps) { @@ -41,7 +49,7 @@ export function NumberTypesTable({ promises }: NumberTypesTableProps) { ] const { table } = useDataTable({ - data: rawData[0].data as any, + data: rawData[0].data, columns, pageCount: rawData[0].pageCount, enablePinning: true, |
