'use server'; import db from '@/db/db'; import { revalidateI18nPaths } from '@/lib/revalidate'; import { and, asc, count, desc, eq, ilike, or, } from 'drizzle-orm'; import { sql } from 'drizzle-orm'; import { approvalLines } from '@/db/schema/knox/approvals'; import { filterColumns } from '@/lib/filter-columns'; // --------------------------------------------- // Types // --------------------------------------------- export type ApprovalLine = typeof approvalLines.$inferSelect; // --------------------------------------------- // Revalidation helpers // --------------------------------------------- async function revalidateApprovalLinesPaths() { await revalidateI18nPaths('/evcp/approval/line'); } export interface ApprovalLineWithUsage extends ApprovalLine { templateCount: number; // 사용 중인 템플릿 수 } // --------------------------------------------- // List & read helpers // --------------------------------------------- interface ListInput { page: number; perPage: number; search?: string; filters?: Record[]; joinOperator?: 'and' | 'or'; sort?: Array<{ id: string; desc: boolean }>; } export async function getApprovalLineList(input: ListInput) { const offset = (input.page - 1) * input.perPage; /* ------------------------------------------------------------------ * WHERE 절 구성 * ----------------------------------------------------------------*/ const advancedWhere = filterColumns({ table: approvalLines, // eslint-disable-next-line @typescript-eslint/no-explicit-any filters: (input.filters ?? []) as any, joinOperator: (input.joinOperator ?? 'and') as 'and' | 'or', }); // 전역 검색 (name, description) let globalWhere; if (input.search) { const s = `%${input.search}%`; globalWhere = or( ilike(approvalLines.name, s), ilike(approvalLines.description, s), ); } const conditions = []; if (advancedWhere) conditions.push(advancedWhere); if (globalWhere) conditions.push(globalWhere); const where = conditions.length === 0 ? undefined : conditions.length === 1 ? conditions[0] : and(...conditions); /* ------------------------------------------------------------------ * ORDER BY 절 구성 * ----------------------------------------------------------------*/ let orderBy; try { orderBy = input.sort && input.sort.length > 0 ? input.sort .map((item) => { if (!item || !item.id || typeof item.id !== 'string') return null; if (!(item.id in approvalLines)) return null; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const col = approvalLines[item.id]; return item.desc ? desc(col) : asc(col); }) .filter((v): v is Exclude => v !== null) : [desc(approvalLines.updatedAt)]; } catch { orderBy = [desc(approvalLines.updatedAt)]; } /* ------------------------------------------------------------------ * 데이터 조회 * ----------------------------------------------------------------*/ const data = await db .select() .from(approvalLines) .where(where) .orderBy(...orderBy) .limit(input.perPage) .offset(offset); const totalResult = await db .select({ count: count() }) .from(approvalLines) .where(where); const total = totalResult[0]?.count ?? 0; const pageCount = Math.ceil(total / input.perPage); return { data, pageCount, }; } // ---------------------------------------------------- // Simple list for options (id, name) // ---------------------------------------------------- // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function getApprovalLineOptions(category?: string): Promise> { const where = category ? eq(approvalLines.category, category) : undefined; const rows = await db .select({ id: approvalLines.id, name: approvalLines.name, aplns: approvalLines.aplns, category: approvalLines.category }) .from(approvalLines) // eslint-disable-next-line @typescript-eslint/no-explicit-any .where(where as any) .orderBy(asc(approvalLines.name)); // eslint-disable-next-line @typescript-eslint/no-explicit-any return rows as Array<{ id: string; name: string; aplns: any[]; category: string | null }>; } // ---------------------------------------------------- // Distinct categories for filter // ---------------------------------------------------- export async function getApprovalLineCategories(): Promise { const rows = await db .select({ category: approvalLines.category }) .from(approvalLines) .where(sql`${approvalLines.category} IS NOT NULL AND ${approvalLines.category} <> ''`) .groupBy(approvalLines.category) .orderBy(asc(approvalLines.category)); return rows.map((r) => r.category!).filter(Boolean); } // ---------------------------------------------------- // Server Action for fetching options by category // ---------------------------------------------------- export async function getApprovalLineOptionsAction(category?: string) { try { const data = await getApprovalLineOptions(category) return { success: true, data } } catch (error) { return { success: false, error: error instanceof Error ? error.message : '조회에 실패했습니다.' } } } // ---------------------------------------------------- // Server Action for fetching distinct categories // ---------------------------------------------------- export async function getApprovalLineCategoriesAction() { try { const data = await getApprovalLineCategories() return { success: true, data } } catch (error) { return { success: false, error: error instanceof Error ? error.message : '카테고리 조회에 실패했습니다.' } } } // ---------------------------------------------------- // Get single approval line // ---------------------------------------------------- export async function getApprovalLine(id: string): Promise { const [line] = await db .select() .from(approvalLines) .where(eq(approvalLines.id, id)) .limit(1); return line || null; } // ---------------------------------------------------- // Create approval line // ---------------------------------------------------- interface CreateInput { name: string; description?: string; category?: string | null; // eslint-disable-next-line @typescript-eslint/no-explicit-any aplns: any[]; // 결재선 구성 (JSON) createdBy: number; } export async function createApprovalLine(data: CreateInput): Promise { // 중복 이름 체크 const existing = await db .select({ id: approvalLines.id }) .from(approvalLines) .where(eq(approvalLines.name, data.name)) .limit(1); if (existing.length > 0) { throw new Error('이미 존재하는 결재선 이름입니다.'); } const [newLine] = await db .insert(approvalLines) .values({ name: data.name, description: data.description, category: data.category ?? null, // eslint-disable-next-line @typescript-eslint/no-explicit-any aplns: data.aplns as any, createdBy: data.createdBy, }) .returning(); await revalidateApprovalLinesPaths(); return newLine; } // ---------------------------------------------------- // Update approval line // ---------------------------------------------------- interface UpdateInput { name?: string; description?: string; category?: string | null; // eslint-disable-next-line @typescript-eslint/no-explicit-any aplns?: any[]; // 결재선 구성 (JSON) updatedBy: number; } export async function updateApprovalLine(id: string, data: UpdateInput): Promise { const existing = await getApprovalLine(id); if (!existing) throw new Error('결재선을 찾을 수 없습니다.'); // 이름 중복 체크 (자신 제외) if (data.name && data.name !== existing.name) { const duplicate = await db .select({ id: approvalLines.id }) .from(approvalLines) .where( and( eq(approvalLines.name, data.name), eq(approvalLines.id, id) ) ) .limit(1); if (duplicate.length > 0) { throw new Error('이미 존재하는 결재선 이름입니다.'); } } // 결재선 업데이트 await db .update(approvalLines) .set({ name: data.name ?? existing.name, description: data.description ?? existing.description, category: data.category ?? existing.category, // eslint-disable-next-line @typescript-eslint/no-explicit-any aplns: (data.aplns as any) ?? (existing.aplns as any), updatedAt: new Date(), }) .where(eq(approvalLines.id, id)); const result = await getApprovalLine(id); if (!result) throw new Error('업데이트된 결재선을 조회할 수 없습니다.'); await revalidateApprovalLinesPaths(); return result; } // ---------------------------------------------------- // Server Actions // ---------------------------------------------------- export async function updateApprovalLineAction(id: string, data: UpdateInput) { try { const updated = await updateApprovalLine(id, data) return { success: true, data: updated } } catch (error) { return { success: false, error: error instanceof Error ? error.message : '업데이트에 실패했습니다.' } } } // ---------------------------------------------------- // Duplicate approval line // ---------------------------------------------------- export async function duplicateApprovalLine( id: string, newName: string, createdBy: number, ): Promise<{ success: boolean; error?: string; data?: ApprovalLine }> { try { const existing = await getApprovalLine(id) if (!existing) return { success: false, error: '결재선을 찾을 수 없습니다.' } // 새 결재선 생성 const duplicated = await createApprovalLine({ name: newName, description: existing.description ? `${existing.description} (복사본)` : undefined, category: existing.category ?? null, // eslint-disable-next-line @typescript-eslint/no-explicit-any aplns: existing.aplns as any, createdBy, }) return { success: true, data: duplicated } } catch (error) { return { success: false, error: error instanceof Error ? error.message : '복제에 실패했습니다.' } } } // ---------------------------------------------------- // Delete (soft delete X -> 실제 삭제) // ---------------------------------------------------- export async function deleteApprovalLine(id: string): Promise<{ success: boolean; error?: string }> { try { await db.delete(approvalLines).where(eq(approvalLines.id, id)); await revalidateApprovalLinesPaths(); return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : '삭제에 실패했습니다.', }; } } // ---------------------------------------------------- // Get approval line usage (템플릿에서 사용 중인지 확인) // ---------------------------------------------------- export async function getApprovalLineUsage(): Promise<{ templateCount: number }> { // 현재는 approvalLines가 템플릿과 직접 연결되지 않으므로 0 반환 // 추후 템플릿에서 결재선을 참조하는 구조로 변경 시 실제 사용량 계산 return { templateCount: 0 }; }