diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/basic-contract/actions.ts | 135 | ||||
| -rw-r--r-- | lib/basic-contract/template/basic-contract-template.tsx | 12 | ||||
| -rw-r--r-- | lib/basic-contract/template/basicContract-table-toolbar-actions.tsx | 34 | ||||
| -rw-r--r-- | lib/basic-contract/template/dispose-documents-dialog.tsx | 122 |
4 files changed, 298 insertions, 5 deletions
diff --git a/lib/basic-contract/actions.ts b/lib/basic-contract/actions.ts new file mode 100644 index 00000000..0af9b948 --- /dev/null +++ b/lib/basic-contract/actions.ts @@ -0,0 +1,135 @@ +"use server" + +import { revalidateTag, revalidatePath } from 'next/cache' +import db from '@/db/db' +import { basicContractTemplates } from '@/db/schema' +import { eq, inArray } from 'drizzle-orm' + +export async function disposeDocuments(documentIds: number[]) { + try { + if (!documentIds || !Array.isArray(documentIds) || documentIds.length === 0) { + throw new Error('폐기할 문서 ID가 필요합니다.') + } + + // 문서들을 DISPOSED 상태로 업데이트하고 폐기일시 설정 + await db + .update(basicContractTemplates) + .set({ + status: 'DISPOSED', + disposedAt: new Date(), + updatedAt: new Date(), + }) + .where(inArray(basicContractTemplates.id, documentIds)) + + // 캐시 무효화 + revalidateTag('basic-contract-templates') + revalidatePath('/evcp/basic-contract-template') + + return { + success: true, + message: `${documentIds.length}개의 문서가 폐기되었습니다.`, + disposedCount: documentIds.length + } + + } catch (error) { + console.error('문서 폐기 처리 오류:', error) + throw new Error('문서 폐기 처리 중 오류가 발생했습니다.') + } +} + +export async function restoreDocuments(documentIds: number[]) { + try { + if (!documentIds || !Array.isArray(documentIds) || documentIds.length === 0) { + throw new Error('복구할 문서 ID가 필요합니다.') + } + + // 문서들을 ACTIVE 상태로 업데이트하고 폐기일시 제거 + await db + .update(basicContractTemplates) + .set({ + status: 'ACTIVE', + disposedAt: null, + updatedAt: new Date(), + }) + .where(inArray(basicContractTemplates.id, documentIds)) + + // 캐시 무효화 + revalidateTag('basic-contract-templates') + revalidatePath('/evcp/basic-contract-template') + + return { + success: true, + message: `${documentIds.length}개의 문서가 복구되었습니다.`, + restoredCount: documentIds.length + } + + } catch (error) { + console.error('문서 복구 처리 오류:', error) + throw new Error('문서 복구 처리 중 오류가 발생했습니다.') + } +} + +export async function createDocumentRevisionAction(input: { + baseDocumentId: number; + contractTemplateName: string; + contractTemplateType: string; + revision: number; + legalReviewRequired: boolean; + fileName: string; + filePath: string; +}) { + try { + const { createBasicContractTemplateRevision } = await import('./service'); + + const { data, error } = await createBasicContractTemplateRevision({ + ...input, + status: 'ACTIVE' as const + }); + + if (error) { + throw new Error(error); + } + + return { + success: true, + data, + message: `${input.contractTemplateName} v${input.revision} 리비전이 성공적으로 생성되었습니다.` + }; + + } catch (error) { + console.error('문서 리비전 생성 오류:', error); + throw new Error(error instanceof Error ? error.message : '문서 리비전 생성 중 오류가 발생했습니다.'); + } +} + +// 업로드 완료 후 문서 생성 (클라이언트에서 직접 호출 가능한 서버 액션) +export async function createDocumentFromUpload(input: { + contractTemplateType: string + contractTemplateName: string + legalReviewRequired: boolean + fileName: string + filePath: string +}) { + try { + const { createBasicContractTemplate } = await import('./service'); + + const { data, error } = await createBasicContractTemplate({ + contractTemplateType: input.contractTemplateType, + contractTemplateName: input.contractTemplateName, + revision: 1, + status: 'ACTIVE', + legalReviewRequired: input.legalReviewRequired, + fileName: input.fileName, + filePath: input.filePath, + } as any) + + if (error) throw new Error(error) + + revalidateTag('basic-contract-templates') + revalidatePath('/evcp/basic-contract-template') + + return { success: true, id: data?.id } + } catch (e: any) { + return { success: false, error: e?.message || '문서 생성 실패' } + } +} diff --git a/lib/basic-contract/template/basic-contract-template.tsx b/lib/basic-contract/template/basic-contract-template.tsx index 8ac421f5..ba24187b 100644 --- a/lib/basic-contract/template/basic-contract-template.tsx +++ b/lib/basic-contract/template/basic-contract-template.tsx @@ -14,6 +14,7 @@ import { getColumns } from "./basic-contract-template-columns"; import { DeleteTemplatesDialog } from "./delete-basicContract-dialog"; import { UpdateTemplateSheet } from "./update-basicContract-sheet"; import { CreateRevisionDialog } from "./create-revision-dialog"; +import { DisposeDocumentsDialog } from "./dispose-documents-dialog"; import { TemplateTableToolbarActions } from "./basicContract-table-toolbar-actions"; import { BasicContractTemplate } from "@/db/schema"; @@ -104,6 +105,17 @@ export function BasicContractTemplateTable({ promises }: BasicTemplateTableProps router.refresh(); }} /> + + <DisposeDocumentsDialog + open={rowAction?.type === "dispose" || rowAction?.type === "restore"} + onOpenChange={() => setRowAction(null)} + documents={rowAction?.row.original ? [rowAction?.row.original] : []} + showTrigger={false} + onSuccess={() => { + setRowAction(null); + router.refresh(); + }} + /> </> ); }
\ No newline at end of file diff --git a/lib/basic-contract/template/basicContract-table-toolbar-actions.tsx b/lib/basic-contract/template/basicContract-table-toolbar-actions.tsx index 439fea26..850dc0a5 100644 --- a/lib/basic-contract/template/basicContract-table-toolbar-actions.tsx +++ b/lib/basic-contract/template/basicContract-table-toolbar-actions.tsx @@ -9,6 +9,7 @@ import { exportTableToExcel } from "@/lib/export" import { Button } from "@/components/ui/button" import { DeleteTemplatesDialog } from "./delete-basicContract-dialog" import { AddTemplateDialog } from "./add-basic-contract-template-dialog" +import { DisposeDocumentsDialog } from "./dispose-documents-dialog" import { BasicContractTemplate } from "@/db/schema" interface TemplateTableToolbarActionsProps { @@ -17,16 +18,39 @@ interface TemplateTableToolbarActionsProps { export function TemplateTableToolbarActions({ table }: TemplateTableToolbarActionsProps) { // 파일 input을 숨기고, 버튼 클릭 시 참조해 클릭하는 방식 - + const selectedRows = table.getFilteredSelectedRowModel().rows + const selectedDocuments = selectedRows.map((row) => row.original) + + // 선택된 문서들의 상태 확인 + const hasActiveDocuments = selectedDocuments.some(doc => doc.status === 'ACTIVE') + const hasDisposedDocuments = selectedDocuments.some(doc => doc.status === 'DISPOSED') return ( <div className="flex items-center gap-2"> {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */} - {table.getFilteredSelectedRowModel().rows.length > 0 ? ( + {selectedRows.length > 0 ? ( <DeleteTemplatesDialog - templates={table - .getFilteredSelectedRowModel() - .rows.map((row) => row.original)} + templates={selectedDocuments} + onSuccess={() => table.toggleAllRowsSelected(false)} + /> + ) : null} + + {/** 2) 선택된 활성 문서가 있으면 폐기 다이얼로그 */} + {selectedRows.length > 0 && hasActiveDocuments ? ( + <DisposeDocumentsDialog + open={false} + onOpenChange={() => {}} + documents={selectedDocuments.filter(doc => doc.status === 'ACTIVE')} + onSuccess={() => table.toggleAllRowsSelected(false)} + /> + ) : null} + + {/** 3) 선택된 폐기 문서가 있으면 복구 다이얼로그 */} + {selectedRows.length > 0 && hasDisposedDocuments ? ( + <DisposeDocumentsDialog + open={false} + onOpenChange={() => {}} + documents={selectedDocuments.filter(doc => doc.status === 'DISPOSED')} onSuccess={() => table.toggleAllRowsSelected(false)} /> ) : null} diff --git a/lib/basic-contract/template/dispose-documents-dialog.tsx b/lib/basic-contract/template/dispose-documents-dialog.tsx new file mode 100644 index 00000000..1154c246 --- /dev/null +++ b/lib/basic-contract/template/dispose-documents-dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import { toast } from "sonner" +import { Trash2, RotateCcw } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { BasicContractTemplate } from "@/db/schema" +import { disposeDocuments, restoreDocuments } from "../actions" + +interface DisposeDocumentsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + documents: BasicContractTemplate[] + showTrigger?: boolean + onSuccess?: () => void +} + +export function DisposeDocumentsDialog({ + open, + onOpenChange, + documents, + showTrigger = true, + onSuccess, +}: DisposeDocumentsDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + + const isDisposeAction = documents.some(doc => doc.status === 'ACTIVE') + const actionText = isDisposeAction ? '폐기' : '복구' + const actionIcon = isDisposeAction ? Trash2 : RotateCcw + + const handleAction = async () => { + if (documents.length === 0) return + + setIsLoading(true) + try { + const documentIds = documents.map(doc => doc.id) + + if (isDisposeAction) { + await disposeDocuments(documentIds) + toast.success(`${documents.length}개의 문서가 폐기되었습니다.`) + } else { + await restoreDocuments(documentIds) + toast.success(`${documents.length}개의 문서가 복구되었습니다.`) + } + + onSuccess?.() + onOpenChange(false) + } catch (error) { + console.error(`${actionText} 처리 오류:`, error) + toast.error(`${actionText} 처리 중 오류가 발생했습니다.`) + } finally { + setIsLoading(false) + } + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + {showTrigger && ( + <Button variant="outline" size="sm"> + {React.createElement(actionIcon, { className: "mr-2 h-4 w-4" })} + {actionText}하기 + </Button> + )} + <DialogContent className="sm:max-w-[425px]"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + {React.createElement(actionIcon, { className: "h-5 w-5" })} + 문서 {actionText} + </DialogTitle> + <DialogDescription> + 선택한 {documents.length}개의 문서를 {actionText}하시겠습니까? + {isDisposeAction + ? ' 폐기된 문서는 복구할 수 있습니다.' + : ' 복구된 문서는 다시 사용할 수 있습니다.' + } + </DialogDescription> + </DialogHeader> + + <div className="py-4"> + <div className="space-y-2"> + {documents.map((doc) => ( + <div key={doc.id} className="flex items-center p-2 bg-muted rounded"> + <div> + <p className="font-medium">{doc.templateName}</p> + <p className="text-sm text-muted-foreground"> + {doc.fileName} • v{doc.revision} + </p> + </div> + </div> + ))} + </div> + </div> + + <DialogFooter> + <Button + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isLoading} + > + 취소 + </Button> + <Button + onClick={handleAction} + disabled={isLoading} + variant={isDisposeAction ? "destructive" : "default"} + > + {isLoading ? "처리 중..." : `${actionText}하기`} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +} |
