diff options
| author | joonhoekim <26rote@gmail.com> | 2025-08-11 09:34:40 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-08-11 09:34:40 +0000 |
| commit | bcd462d6e60871b86008e072f4b914138fc5c328 (patch) | |
| tree | c22876fd6c6e7e48254587848b9dff50cdb8b032 /lib/approval-line/service.ts | |
| parent | cbb4c7fe0b94459162ad5e998bc05cd293e0ff96 (diff) | |
(김준회) 리치텍스트에디터 (결재템플릿을 위한 공통컴포넌트), command-menu 에러 수정, 결재 템플릿 관리, 결재선 관리, ECC RFQ+PR Item 수신시 비즈니스테이블(ProcurementRFQ) 데이터 적재, WSDL 오류 수정
Diffstat (limited to 'lib/approval-line/service.ts')
| -rw-r--r-- | lib/approval-line/service.ts | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/lib/approval-line/service.ts b/lib/approval-line/service.ts new file mode 100644 index 00000000..3000e25f --- /dev/null +++ b/lib/approval-line/service.ts @@ -0,0 +1,341 @@ +'use server'; + +import db from '@/db/db'; +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; + +export interface ApprovalLineWithUsage extends ApprovalLine { + templateCount: number; // 사용 중인 템플릿 수 +} + +// --------------------------------------------- +// List & read helpers +// --------------------------------------------- + +interface ListInput { + page: number; + perPage: number; + search?: string; + filters?: Record<string, unknown>[]; + 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<typeof v, null> => 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<Array<{ id: string; name: string; aplns: any[]; category: string | null }>> { + 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<string[]> { + 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<ApprovalLine | null> { + 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; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + aplns: any[]; // 결재선 구성 (JSON) + createdBy: number; +} + +export async function createApprovalLine(data: CreateInput): Promise<ApprovalLine> { + // 중복 이름 체크 + 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, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + aplns: data.aplns as any, + createdBy: data.createdBy, + }) + .returning(); + + return newLine; +} + +// ---------------------------------------------------- +// Update approval line +// ---------------------------------------------------- +interface UpdateInput { + name?: string; + description?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + aplns?: any[]; // 결재선 구성 (JSON) + updatedBy: number; +} + +export async function updateApprovalLine(id: string, data: UpdateInput): Promise<ApprovalLine> { + 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, + // 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('업데이트된 결재선을 조회할 수 없습니다.'); + 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, + // 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)); + 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 }; +}
\ No newline at end of file |
