diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-30 16:48:52 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-30 16:48:52 +0900 |
| commit | 567baf74e62bb71d44604eb5fe3457f773396678 (patch) | |
| tree | d917f36c85916e500fb6b3043841dd346235c07f /lib/approval-template/category-service.ts | |
| parent | 0cd76046aa7c5426d42740f9acb06c42e4d7e686 (diff) | |
(김준회) 결재 카테고리 로직 개선, 미사용 코드 제거
Diffstat (limited to 'lib/approval-template/category-service.ts')
| -rw-r--r-- | lib/approval-template/category-service.ts | 271 |
1 files changed, 271 insertions, 0 deletions
diff --git a/lib/approval-template/category-service.ts b/lib/approval-template/category-service.ts new file mode 100644 index 00000000..8f6f93c8 --- /dev/null +++ b/lib/approval-template/category-service.ts @@ -0,0 +1,271 @@ +'use server'; + +import db from '@/db/db'; +import { revalidateI18nPaths } from '@/lib/revalidate'; +import { + and, + asc, + count, + desc, + eq, + ilike, + or, +} from 'drizzle-orm'; + +import { approvalTemplateCategories, approvalTemplates } from '@/db/schema/knox/approvals'; + +// --------------------------------------------- +// Types +// --------------------------------------------- + +export type ApprovalTemplateCategory = typeof approvalTemplateCategories.$inferSelect; + +// --------------------------------------------- +// Revalidation helpers +// --------------------------------------------- +async function revalidateApprovalTemplateCategoriesPaths() { + await revalidateI18nPaths('/evcp/approval/template'); +} + +// --------------------------------------------- +// 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 getApprovalTemplateCategoryList(input: ListInput) { + const offset = (input.page - 1) * input.perPage; + + /* ------------------------------------------------------------------ + * WHERE 절 구성 + * ----------------------------------------------------------------*/ + const advancedWhere = input.filters ? and( + ...input.filters.map(filter => { + // 간단한 필터링 로직 - 실제로는 filterColumns 유틸리티 사용 가능 + return eq(approvalTemplateCategories.isActive, true); + }) + ) : undefined; + + // 전역 검색 (name, description) + let globalWhere; + if (input.search) { + const s = `%${input.search}%`; + globalWhere = or( + ilike(approvalTemplateCategories.name, s), + ilike(approvalTemplateCategories.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 approvalTemplateCategories)) return null; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const col = approvalTemplateCategories[item.id]; + return item.desc ? desc(col) : asc(col); + }) + .filter((v): v is Exclude<typeof v, null> => v !== null) + : [asc(approvalTemplateCategories.sortOrder), asc(approvalTemplateCategories.name)]; + } catch { + orderBy = [asc(approvalTemplateCategories.sortOrder), asc(approvalTemplateCategories.name)]; + } + + /* ------------------------------------------------------------------ + * 데이터 조회 + * ----------------------------------------------------------------*/ + const data = await db + .select() + .from(approvalTemplateCategories) + .where(where) + .orderBy(...orderBy) + .limit(input.perPage) + .offset(offset); + + const totalResult = await db + .select({ count: count() }) + .from(approvalTemplateCategories) + .where(where); + + const total = totalResult[0]?.count ?? 0; + const pageCount = Math.ceil(total / input.perPage); + + return { + data, + pageCount, + }; +} + +// ---------------------------------------------------- +// Get single category +// ---------------------------------------------------- +export async function getApprovalTemplateCategory(id: string): Promise<ApprovalTemplateCategory | null> { + const [category] = await db + .select() + .from(approvalTemplateCategories) + .where(eq(approvalTemplateCategories.id, id)) + .limit(1); + + return category || null; +} + +// ---------------------------------------------------- +// Create category +// ---------------------------------------------------- +interface CreateInput { + name: string; + description?: string; + sortOrder?: number; + createdBy: number; +} + +export async function createApprovalTemplateCategory(data: CreateInput): Promise<ApprovalTemplateCategory> { + // 중복 이름 체크 + const existing = await db + .select({ id: approvalTemplateCategories.id }) + .from(approvalTemplateCategories) + .where(and( + eq(approvalTemplateCategories.name, data.name), + eq(approvalTemplateCategories.isActive, true) + )) + .limit(1); + + if (existing.length > 0) { + throw new Error('이미 존재하는 카테고리 이름입니다.'); + } + + const [newCategory] = await db + .insert(approvalTemplateCategories) + .values({ + name: data.name, + description: data.description, + sortOrder: data.sortOrder ?? 0, + createdBy: data.createdBy, + updatedBy: data.createdBy, + }) + .returning(); + + await revalidateApprovalTemplateCategoriesPaths(); + return newCategory; +} + +// ---------------------------------------------------- +// Update category +// ---------------------------------------------------- +interface UpdateInput { + name?: string; + description?: string; + isActive?: boolean; + sortOrder?: number; + updatedBy: number; +} + +export async function updateApprovalTemplateCategory(id: string, data: UpdateInput): Promise<ApprovalTemplateCategory> { + const existing = await getApprovalTemplateCategory(id); + if (!existing) throw new Error('카테고리를 찾을 수 없습니다.'); + + // 이름 변경 시 중복 체크 + if (data.name && data.name !== existing.name) { + const existingName = await db + .select({ id: approvalTemplateCategories.id }) + .from(approvalTemplateCategories) + .where(and( + eq(approvalTemplateCategories.name, data.name), + eq(approvalTemplateCategories.isActive, true) + )) + .limit(1); + + if (existingName.length > 0) { + throw new Error('이미 존재하는 카테고리 이름입니다.'); + } + } + + await db + .update(approvalTemplateCategories) + .set({ + name: data.name ?? existing.name, + description: data.description === undefined ? existing.description : data.description, + isActive: data.isActive ?? existing.isActive, + sortOrder: data.sortOrder ?? existing.sortOrder, + updatedAt: new Date(), + updatedBy: data.updatedBy, + }) + .where(eq(approvalTemplateCategories.id, id)); + + const result = await getApprovalTemplateCategory(id); + if (!result) throw new Error('업데이트된 카테고리를 조회할 수 없습니다.'); + await revalidateApprovalTemplateCategoriesPaths(); + return result; +} + +// ---------------------------------------------------- +// Delete category (soft delete - 비활성화) +// ---------------------------------------------------- +export async function deleteApprovalTemplateCategory(id: string, updatedBy: number): Promise<{ success: boolean; error?: string }> { + try { + const existing = await getApprovalTemplateCategory(id); + if (!existing) return { success: false, error: '카테고리를 찾을 수 없습니다.' }; + + // 카테고리가 사용 중인지 확인 (approvalTemplates에서 참조 확인) + const usageCount = await db + .select({ count: count() }) + .from(approvalTemplates) + .where(eq(approvalTemplates.category, existing.name)); + + if (usageCount[0]?.count > 0) { + return { success: false, error: '사용 중인 카테고리는 삭제할 수 없습니다.' }; + } + + await db + .update(approvalTemplateCategories) + .set({ + isActive: false, + updatedAt: new Date(), + updatedBy, + }) + .where(eq(approvalTemplateCategories.id, id)); + + await revalidateApprovalTemplateCategoriesPaths(); + return { success: true }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '삭제에 실패했습니다.', + }; + } +} + +// ---------------------------------------------------- +// Get all active categories (드롭다운용) +// ---------------------------------------------------- +export async function getActiveApprovalTemplateCategories(): Promise<ApprovalTemplateCategory[]> { + return await db + .select() + .from(approvalTemplateCategories) + .where(eq(approvalTemplateCategories.isActive, true)) + .orderBy(asc(approvalTemplateCategories.sortOrder), asc(approvalTemplateCategories.name)); +} |
