summaryrefslogtreecommitdiff
path: root/lib/approval-line/service.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-08-11 09:34:40 +0000
committerjoonhoekim <26rote@gmail.com>2025-08-11 09:34:40 +0000
commitbcd462d6e60871b86008e072f4b914138fc5c328 (patch)
treec22876fd6c6e7e48254587848b9dff50cdb8b032 /lib/approval-line/service.ts
parentcbb4c7fe0b94459162ad5e998bc05cd293e0ff96 (diff)
(김준회) 리치텍스트에디터 (결재템플릿을 위한 공통컴포넌트), command-menu 에러 수정, 결재 템플릿 관리, 결재선 관리, ECC RFQ+PR Item 수신시 비즈니스테이블(ProcurementRFQ) 데이터 적재, WSDL 오류 수정
Diffstat (limited to 'lib/approval-line/service.ts')
-rw-r--r--lib/approval-line/service.ts341
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