From 356929b399ef31a4de82906267df438cf29ea59d Mon Sep 17 00:00:00 2001 From: 0-Zz-ang Date: Thu, 10 Jul 2025 15:56:13 +0900 Subject: 인터페이스 관련 파일 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/integration/service.ts | 226 +++++++++++++++++ .../table/delete-integration-dialog.tsx | 154 ++++++++++++ lib/integration/table/integration-add-dialog.tsx | 272 ++++++++++++++++++++ .../table/integration-delete-dialog.tsx | 122 +++++++++ lib/integration/table/integration-edit-dialog.tsx | 274 ++++++++++++++++++++ lib/integration/table/integration-edit-sheet.tsx | 278 +++++++++++++++++++++ .../table/integration-table-columns.tsx | 214 ++++++++++++++++ .../table/integration-table-toolbar.tsx | 53 ++++ lib/integration/table/integration-table.tsx | 166 ++++++++++++ lib/integration/validations.ts | 99 ++++++++ 10 files changed, 1858 insertions(+) create mode 100644 lib/integration/service.ts create mode 100644 lib/integration/table/delete-integration-dialog.tsx create mode 100644 lib/integration/table/integration-add-dialog.tsx create mode 100644 lib/integration/table/integration-delete-dialog.tsx create mode 100644 lib/integration/table/integration-edit-dialog.tsx create mode 100644 lib/integration/table/integration-edit-sheet.tsx create mode 100644 lib/integration/table/integration-table-columns.tsx create mode 100644 lib/integration/table/integration-table-toolbar.tsx create mode 100644 lib/integration/table/integration-table.tsx create mode 100644 lib/integration/validations.ts (limited to 'lib/integration') diff --git a/lib/integration/service.ts b/lib/integration/service.ts new file mode 100644 index 00000000..ad644ca4 --- /dev/null +++ b/lib/integration/service.ts @@ -0,0 +1,226 @@ +"use server"; + +import db from "@/db/db"; +import { integrations } from "@/db/schema/integration"; +import { eq } from "drizzle-orm"; +import { GetIntegrationsSchema } from "./validations"; +import { filterColumns } from "@/lib/filter-columns"; +import { asc, desc, ilike, and, or, count } from "drizzle-orm"; + +/* ----------------------------------------------------- + 1) 통합 목록 조회 +----------------------------------------------------- */ +export async function getIntegrations(input: GetIntegrationsSchema) { + try { + const offset = (input.page - 1) * input.perPage; + + // 1. where 절 + let advancedWhere; + try { + advancedWhere = filterColumns({ + table: integrations, + filters: input.filters, + joinOperator: input.joinOperator, + }); + } catch (whereErr) { + console.error("Error building advanced where:", whereErr); + advancedWhere = undefined; + } + + let globalWhere; + if (input.search) { + try { + const s = `%${input.search}%`; + globalWhere = or( + ilike(integrations.code, s), + ilike(integrations.name, s), + ilike(integrations.sourceSystem, s), + ilike(integrations.targetSystem, s), + ilike(integrations.description, s) + ); + } catch (searchErr) { + console.error("Error building search where:", searchErr); + globalWhere = undefined; + } + } + + // 2. where 결합 + let finalWhere; + const whereArr = [advancedWhere, globalWhere].filter(Boolean); + if (whereArr.length === 2) { + finalWhere = and(...whereArr); + } else if (whereArr.length === 1) { + finalWhere = whereArr[0]; + } else { + finalWhere = undefined; + } + + // 3. order by + let orderBy = [asc(integrations.createdAt)]; + try { + if (input.sort.length > 0) { + const sortItems = input.sort + .map((item) => { + if (!item || !item.id || typeof item.id !== "string") return null; + + // 기본 정렬 컬럼들만 허용 + switch (item.id) { + case "id": + return item.desc ? desc(integrations.id) : asc(integrations.id); + case "code": + return item.desc ? desc(integrations.code) : asc(integrations.code); + case "name": + return item.desc ? desc(integrations.name) : asc(integrations.name); + case "type": + return item.desc ? desc(integrations.type) : asc(integrations.type); + case "status": + return item.desc ? desc(integrations.status) : asc(integrations.status); + case "sourceSystem": + return item.desc ? desc(integrations.sourceSystem) : asc(integrations.sourceSystem); + case "targetSystem": + return item.desc ? desc(integrations.targetSystem) : asc(integrations.targetSystem); + case "createdAt": + return item.desc ? desc(integrations.createdAt) : asc(integrations.createdAt); + case "updatedAt": + return item.desc ? desc(integrations.updatedAt) : asc(integrations.updatedAt); + default: + return null; + } + }) + .filter((v): v is Exclude => v !== null); + + if (sortItems.length > 0) { + orderBy = sortItems; + } + } + } catch (orderErr) { + console.error("Error building order by:", orderErr); + } + + // 4. 쿼리 실행 + let data = []; + let total = 0; + + try { + const queryBuilder = db.select().from(integrations); + + if (finalWhere !== undefined) { + queryBuilder.where(finalWhere); + } + + if (orderBy && orderBy.length > 0) { + queryBuilder.orderBy(...orderBy); + } + if (typeof offset === "number" && !isNaN(offset)) { + queryBuilder.offset(offset); + } + if (typeof input.perPage === "number" && !isNaN(input.perPage)) { + queryBuilder.limit(input.perPage); + } + + data = await queryBuilder; + + const countBuilder = db + .select({ count: count() }) + .from(integrations); + + if (finalWhere !== undefined) { + countBuilder.where(finalWhere); + } + + const countResult = await countBuilder; + total = countResult[0]?.count || 0; + } catch (queryErr) { + console.error("Query execution failed:", queryErr); + throw queryErr; + } + + const pageCount = Math.ceil(total / input.perPage); + + return { data, pageCount }; + } catch (err) { + console.error("Error in getIntegrations:", err); + if (err instanceof Error) { + console.error("Error message:", err.message); + console.error("Error stack:", err.stack); + } + return { data: [], pageCount: 0 }; + } +} + +/* ----------------------------------------------------- + 2) 통합 생성 +----------------------------------------------------- */ +export async function createIntegration(data: Omit) { + try { + const [created] = await db.insert(integrations).values({ + ...data, + createdAt: new Date(), + updatedAt: new Date(), + }).returning(); + return { data: created }; + } catch (err) { + console.error("Error creating integration:", err); + return { error: "생성 중 오류가 발생했습니다." }; + } +} + +/* ----------------------------------------------------- + 3) 통합 수정 +----------------------------------------------------- */ +export async function updateIntegration(id: number, data: Partial) { + try { + const [updated] = await db + .update(integrations) + .set({ + ...data, + updatedAt: new Date(), + }) + .where(eq(integrations.id, id)) + .returning(); + return { data: updated }; + } catch (err) { + console.error("Error updating integration:", err); + return { error: "수정 중 오류가 발생했습니다." }; + } +} + +/* ----------------------------------------------------- + 4) 통합 삭제 +----------------------------------------------------- */ +export async function deleteIntegration(id: number) { + try { + await db.delete(integrations).where(eq(integrations.id, id)); + return { success: true }; + } catch (err) { + console.error("Error deleting integration:", err); + return { error: "삭제 중 오류가 발생했습니다." }; + } +} + +// 통합 조회 (단일) +export async function getIntegration(id: number): Promise> { + try { + const result = await db.select().from(integrations).where(eq(integrations.id, id)).limit(1); + + if (result.length === 0) { + return { error: "통합을 찾을 수 없습니다." }; + } + + return { data: result[0] }; + } catch (error) { + console.error("통합 조회 오류:", error); + return { error: "통합 조회에 실패했습니다." }; + } +} + +// 기존 함수들도 유지 (하위 호환성) +export async function getIntegrationList() { + try { + const data = await db.select().from(integrations); + return data; + } catch (error) { + console.error("통합 목록 조회 실패:", error); + return []; + } +} \ No newline at end of file diff --git a/lib/integration/table/delete-integration-dialog.tsx b/lib/integration/table/delete-integration-dialog.tsx new file mode 100644 index 00000000..5ce9676d --- /dev/null +++ b/lib/integration/table/delete-integration-dialog.tsx @@ -0,0 +1,154 @@ +"use client" + +import * as React from "react" +import { type Row } from "@tanstack/react-table" +import { Loader, Trash } from "lucide-react" +import { toast } from "sonner" + +import { useMediaQuery } from "@/hooks/use-media-query" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" + +import { deleteIntegration } from "../service" +import { integrations } from "@/db/schema/integration" + +interface DeleteIntegrationDialogProps + extends React.ComponentPropsWithoutRef { + integrations: Row["original"][] + showTrigger?: boolean + onSuccess?: () => void +} + +export function DeleteIntegrationDialog({ + integrations: integrationData = [], + showTrigger = true, + onSuccess, + ...props +}: DeleteIntegrationDialogProps) { + const [isDeletePending, startDeleteTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + + function onDelete() { + startDeleteTransition(async () => { + try { + // 각 통합을 순차적으로 삭제 + for (const integrationItem of integrationData) { + const result = await deleteIntegration(integrationItem.id) + if (!result.success) { + toast.error(`인터페이스 ${integrationItem.name} 삭제 실패: ${result.error}`) + return + } + } + + props.onOpenChange?.(false) + toast.success("인터페이스가 성공적으로 삭제되었습니다.") + onSuccess?.() + } catch (error) { + console.error("Delete error:", error) + toast.error("인터페이스 삭제 중 오류가 발생했습니다.") + } + }) + } + + if (isDesktop) { + return ( + + {showTrigger ? ( + + + + ) : null} + + + 정말로 삭제하시겠습니까? + + 이 작업은 되돌릴 수 없습니다. 선택된{" "} + {integrationData?.length ?? 0} + 개의 인터페이스를 서버에서 영구적으로 삭제합니다. + + + + + + + + + + + ) + } + + return ( + + {showTrigger ? ( + + + + ) : null} + + + 정말로 삭제하시겠습니까? + + 이 작업은 되돌릴 수 없습니다. 선택된{" "} + {integrationData?.length ?? 0} + 개의 인터페이스를 서버에서 영구적으로 삭제합니다. + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/lib/integration/table/integration-add-dialog.tsx b/lib/integration/table/integration-add-dialog.tsx new file mode 100644 index 00000000..aeab2a5f --- /dev/null +++ b/lib/integration/table/integration-add-dialog.tsx @@ -0,0 +1,272 @@ +"use client"; + +import * as React from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { Plus, Loader2 } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { createIntegration } from "../service"; +import { toast } from "sonner"; + +const createIntegrationSchema = z.object({ + code: z.string().min(1, "코드는 필수입니다."), + name: z.string().min(1, "이름은 필수입니다."), + type: z.enum(["rest_api", "soap", "db_to_db"], { required_error: "타입은 필수입니다." }), + description: z.string().optional(), + sourceSystem: z.string().min(1, "소스 시스템은 필수입니다."), + targetSystem: z.string().min(1, "타겟 시스템은 필수입니다."), + status: z.enum(["active", "inactive", "deprecated"]).default("active"), + metadata: z.any().optional(), +}); + +type CreateIntegrationFormValues = z.infer; + +interface IntegrationAddDialogProps { + onSuccess?: () => void; +} + +export function IntegrationAddDialog({ onSuccess }: IntegrationAddDialogProps) { + const [open, setOpen] = React.useState(false); + const [isLoading, setIsLoading] = React.useState(false); + + const form = useForm({ + resolver: zodResolver(createIntegrationSchema), + defaultValues: { + code: "", + name: "", + type: "rest_api", + description: "", + sourceSystem: "", + targetSystem: "", + status: "active", + metadata: {}, + }, + }); + + const handleOpenChange = (newOpen: boolean) => { + setOpen(newOpen); + if (!newOpen) { + form.reset(); + } + }; + + const handleCancel = () => { + form.reset(); + setOpen(false); + }; + + const onSubmit = async (data: CreateIntegrationFormValues) => { + setIsLoading(true); + try { + const result = await createIntegration(data); + if (result.data) { + toast.success("인터페이스가 성공적으로 추가되었습니다."); + form.reset(); + setOpen(false); + if (onSuccess) { + onSuccess(); + } + } else { + toast.error(result.error || "생성 중 오류가 발생했습니다."); + } + } catch (error) { + console.error("인터페이스 생성 오류:", error); + toast.error("인터페이스 생성에 실패했습니다."); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + + + + 새 인터페이스 추가 + + 새로운 인터페이스를 추가합니다. 필수 정보를 입력해주세요. + * 표시된 항목은 필수 입력사항입니다. + + + +
+ + ( + + + 코드 * + + + + + + + )} + /> + + ( + + + 이름 * + + + + + + + )} + /> + + ( + + + 타입 * + + + + + )} + /> + + ( + + + 소스 시스템 * + + + + + + + )} + /> + + ( + + + 타겟 시스템 * + + + + + + + )} + /> + + ( + + + 상태 * + + + + + )} + /> + + ( + + 설명 + +