diff options
Diffstat (limited to 'lib/legal-review/service.ts')
| -rw-r--r-- | lib/legal-review/service.ts | 738 |
1 files changed, 738 insertions, 0 deletions
diff --git a/lib/legal-review/service.ts b/lib/legal-review/service.ts new file mode 100644 index 00000000..bc55a1fc --- /dev/null +++ b/lib/legal-review/service.ts @@ -0,0 +1,738 @@ +'use server' + +import { revalidatePath, unstable_noStore } from "next/cache"; +import db from "@/db/db"; +import { legalWorks, legalWorkRequests, legalWorkResponses, legalWorkAttachments, vendors, legalWorksDetailView } from "@/db/schema"; +import { and, asc, count, desc, eq, ilike, or, SQL, inArray } from "drizzle-orm"; +import { CreateLegalWorkData, GetLegalWorksSchema, createLegalWorkSchema } from "./validations"; +import { filterColumns } from "@/lib/filter-columns"; +import { saveFile } from "../file-stroage"; + +interface CreateLegalWorkResult { + success: boolean; + data?: { + id: number; + message: string; + }; + error?: string; +} + + + +export async function createLegalWork( + data: CreateLegalWorkData +): Promise<CreateLegalWorkResult> { + unstable_noStore(); + + try { + // 1. 입력 데이터 검증 + const validatedData = createLegalWorkSchema.parse(data); + + // 2. 벤더 정보 조회 + const vendor = await db + .select({ + id: vendors.id, + vendorCode: vendors.vendorCode, + vendorName: vendors.vendorName, + }) + .from(vendors) + .where(eq(vendors.id, validatedData.vendorId)) + .limit(1); + + if (!vendor.length) { + return { + success: false, + error: "선택한 벤더를 찾을 수 없습니다.", + }; + } + + const selectedVendor = vendor[0]; + + // 3. 트랜잭션으로 데이터 삽입 + const result = await db.transaction(async (tx) => { + // 3-1. legal_works 테이블에 메인 데이터 삽입 + const [legalWorkResult] = await tx + .insert(legalWorks) + .values({ + category: validatedData.category, + status: "신규등록", // 초기 상태 + vendorId: validatedData.vendorId, + vendorCode: selectedVendor.vendorCode, + vendorName: selectedVendor.vendorName, + isUrgent: validatedData.isUrgent, + requestDate: validatedData.requestDate, + consultationDate: new Date().toISOString().split('T')[0], // 오늘 날짜 + hasAttachment: false, // 초기값 + reviewer: validatedData.reviewer, // 추후 할당 + legalResponder: null, // 추후 할당 + }) + .returning({ id: legalWorks.id }); + + const legalWorkId = legalWorkResult.id; + + + + return { legalWorkId }; + }); + + // 4. 캐시 재검증 + revalidatePath("/legal-works"); + + return { + success: true, + data: { + id: result.legalWorkId, + message: "법무업무가 성공적으로 등록되었습니다.", + }, + }; + + } catch (error) { + console.error("createLegalWork 오류:", error); + + // 데이터베이스 오류 처리 + if (error instanceof Error) { + // 외래키 제약 조건 오류 + if (error.message.includes('foreign key constraint')) { + return { + success: false, + error: "선택한 벤더가 유효하지 않습니다.", + }; + } + + // 중복 키 오류 등 기타 DB 오류 + return { + success: false, + error: "데이터베이스 오류가 발생했습니다.", + }; + } + + return { + success: false, + error: "알 수 없는 오류가 발생했습니다.", + }; + } +} + +// 법무업무 상태 업데이트 함수 (보너스) +export async function updateLegalWorkStatus( + legalWorkId: number, + status: string, + reviewer?: string, + legalResponder?: string +): Promise<CreateLegalWorkResult> { + unstable_noStore(); + + try { + const updateData: Partial<typeof legalWorks.$inferInsert> = { + status, + updatedAt: new Date(), + }; + + if (reviewer) updateData.reviewer = reviewer; + if (legalResponder) updateData.legalResponder = legalResponder; + + await db + .update(legalWorks) + .set(updateData) + .where(eq(legalWorks.id, legalWorkId)); + + revalidatePath("/legal-works"); + + return { + success: true, + data: { + id: legalWorkId, + message: "상태가 성공적으로 업데이트되었습니다.", + }, + }; + + } catch (error) { + console.error("updateLegalWorkStatus 오류:", error); + return { + success: false, + error: "상태 업데이트 중 오류가 발생했습니다.", + }; + } +} + +// 법무업무 삭제 함수 (보너스) +export async function deleteLegalWork(legalWorkId: number): Promise<CreateLegalWorkResult> { + unstable_noStore(); + + try { + await db.transaction(async (tx) => { + // 관련 요청 데이터 먼저 삭제 + await tx + .delete(legalWorkRequests) + .where(eq(legalWorkRequests.legalWorkId, legalWorkId)); + + // 메인 법무업무 데이터 삭제 + await tx + .delete(legalWorks) + .where(eq(legalWorks.id, legalWorkId)); + }); + + revalidatePath("/legal-works"); + + return { + success: true, + data: { + id: legalWorkId, + message: "법무업무가 성공적으로 삭제되었습니다.", + }, + }; + + } catch (error) { + console.error("deleteLegalWork 오류:", error); + return { + success: false, + error: "삭제 중 오류가 발생했습니다.", + }; + } +} + + +export async function getLegalWorks(input: GetLegalWorksSchema) { + unstable_noStore(); // ✅ 1. 캐싱 방지 추가 + + try { + const offset = (input.page - 1) * input.perPage; + + // ✅ 2. 안전한 필터 처리 (getEvaluationTargets와 동일) + let advancedWhere: SQL<unknown> | undefined = undefined; + + if (input.filters && Array.isArray(input.filters) && input.filters.length > 0) { + console.log("필터 적용:", input.filters.map(f => `${f.id} ${f.operator} ${f.value}`)); + + try { + advancedWhere = filterColumns({ + table: legalWorksDetailView, + filters: input.filters, + joinOperator: input.joinOperator || 'and', + }); + + console.log("필터 조건 생성 완료"); + } catch (error) { + console.error("필터 조건 생성 오류:", error); + // ✅ 필터 오류 시에도 전체 데이터 반환 + advancedWhere = undefined; + } + } + + // ✅ 3. 안전한 글로벌 검색 처리 + let globalWhere: SQL<unknown> | undefined = undefined; + if (input.search) { + const searchTerm = `%${input.search}%`; + + const searchConditions: SQL<unknown>[] = [ + ilike(legalWorksDetailView.vendorCode, searchTerm), + ilike(legalWorksDetailView.vendorName, searchTerm), + ilike(legalWorksDetailView.title, searchTerm), + ilike(legalWorksDetailView.requestContent, searchTerm), + ilike(legalWorksDetailView.reviewer, searchTerm), + ilike(legalWorksDetailView.legalResponder, searchTerm) + ].filter(Boolean); + + if (searchConditions.length > 0) { + globalWhere = or(...searchConditions); + } + } + + // ✅ 4. 안전한 WHERE 조건 결합 + const whereConditions: SQL<unknown>[] = []; + if (advancedWhere) whereConditions.push(advancedWhere); + if (globalWhere) whereConditions.push(globalWhere); + + const finalWhere = whereConditions.length > 0 ? and(...whereConditions) : undefined; + + // ✅ 5. 전체 데이터 수 조회 + const totalResult = await db + .select({ count: count() }) + .from(legalWorksDetailView) + .where(finalWhere); + + const total = totalResult[0]?.count || 0; + + if (total === 0) { + return { data: [], pageCount: 0, total: 0 }; + } + + console.log("총 데이터 수:", total); + + // ✅ 6. 정렬 및 페이징 처리 + const orderByColumns = input.sort.map((sort) => { + const column = sort.id as keyof typeof legalWorksDetailView.$inferSelect; + return sort.desc + ? desc(legalWorksDetailView[column]) + : asc(legalWorksDetailView[column]); + }); + + if (orderByColumns.length === 0) { + orderByColumns.push(desc(legalWorksDetailView.createdAt)); + } + + const legalWorksData = await db + .select() + .from(legalWorksDetailView) + .where(finalWhere) + .orderBy(...orderByColumns) + .limit(input.perPage) + .offset(offset); + + const pageCount = Math.ceil(total / input.perPage); + + console.log("반환 데이터 수:", legalWorksData.length); + + return { data: legalWorksData, pageCount, total }; + } catch (err) { + console.error("getLegalWorks 오류:", err); + return { data: [], pageCount: 0, total: 0 }; + } +} +// 특정 법무업무 상세 조회 +export async function getLegalWorkById(id: number) { + unstable_noStore(); + + try { + const result = await db + .select() + .from(legalWorksDetailView) + .where(eq(legalWorksDetailView.id , id)) + .limit(1); + + return result[0] || null; + } catch (error) { + console.error("getLegalWorkById 오류:", error); + return null; + } +} + +// 법무업무 통계 (뷰 테이블 사용) +export async function getLegalWorksStats() { + unstable_noStore(); + try { + // 전체 통계 + const totalStats = await db + .select({ + total: count(), + category: legalWorksDetailView.category, + status: legalWorksDetailView.status, + isUrgent: legalWorksDetailView.isUrgent, + }) + .from(legalWorksDetailView); + + // 통계 데이터 가공 + const stats = { + total: totalStats.length, + byCategory: {} as Record<string, number>, + byStatus: {} as Record<string, number>, + urgent: 0, + }; + + totalStats.forEach(stat => { + // 카테고리별 집계 + if (stat.category) { + stats.byCategory[stat.category] = (stats.byCategory[stat.category] || 0) + 1; + } + + // 상태별 집계 + if (stat.status) { + stats.byStatus[stat.status] = (stats.byStatus[stat.status] || 0) + 1; + } + + // 긴급 건수 + if (stat.isUrgent) { + stats.urgent++; + } + }); + + return stats; + } catch (error) { + console.error("getLegalWorksStatsSimple 오류:", error); + return { + total: 0, + byCategory: {}, + byStatus: {}, + urgent: 0, + }; + } +} + +// 검토요청 폼 데이터 타입 +interface RequestReviewData { + // 기본 설정 + dueDate: string + assignee?: string + notificationMethod: "email" | "internal" | "both" + + // 법무업무 상세 정보 + reviewDepartment: "준법문의" | "법무검토" + inquiryType?: "국내계약" | "국내자문" | "해외계약" | "해외자문" + + // 공통 필드 + title: string + requestContent: string + + // 준법문의 전용 필드 + isPublic?: boolean + + // 법무검토 전용 필드들 + contractProjectName?: string + contractType?: string + contractCounterparty?: string + counterpartyType?: "법인" | "개인" + contractPeriod?: string + contractAmount?: string + factualRelation?: string + projectNumber?: string + shipownerOrderer?: string + projectType?: string + governingLaw?: string +} + +// 첨부파일 업로드 함수 +async function uploadAttachment(file: File, legalWorkId: number, userId?: string) { + try { + console.log(`📎 첨부파일 업로드 시작: ${file.name} (${file.size} bytes)`) + + const result = await saveFile({ + file, + directory: "legal-works", + originalName: file.name, + userId: userId || "system" + }) + + if (!result.success) { + throw new Error(result.error || "파일 업로드 실패") + } + + console.log(`✅ 첨부파일 업로드 성공: ${result.fileName}`) + + return { + fileName: result.fileName!, + originalFileName: result.originalName!, + filePath: result.publicPath!, + fileSize: result.fileSize!, + mimeType: file.type, + securityChecks: result.securityChecks + } + } catch (error) { + console.error(`❌ 첨부파일 업로드 실패: ${file.name}`, error) + throw error + } +} + + +export async function requestReview( + legalWorkId: number, + formData: RequestReviewData, + attachments: File[] = [], + userId?: string +) { + try { + console.log(`🚀 검토요청 처리 시작 - 법무업무 #${legalWorkId}`) + + // 트랜잭션 시작 + const result = await db.transaction(async (tx) => { + // 1. legal_works 테이블 업데이트 + const [updatedWork] = await tx + .update(legalWorks) + .set({ + status: "검토요청", + expectedAnswerDate: formData.dueDate, + hasAttachment: attachments.length > 0, + updatedAt: new Date(), + }) + .where(eq(legalWorks.id, legalWorkId)) + .returning() + + if (!updatedWork) { + throw new Error("법무업무를 찾을 수 없습니다.") + } + + console.log(`📝 법무업무 상태 업데이트 완료: ${updatedWork.status}`) + + // 2. legal_work_requests 테이블에 데이터 삽입 + const [createdRequest] = await tx + .insert(legalWorkRequests) + .values({ + legalWorkId: legalWorkId, + reviewDepartment: formData.reviewDepartment, + inquiryType: formData.inquiryType || null, + title: formData.title, + requestContent: formData.requestContent, + + // 준법문의 관련 필드 + isPublic: formData.reviewDepartment === "준법문의" ? (formData.isPublic || false) : null, + + // 법무검토 관련 필드들 + contractProjectName: formData.contractProjectName || null, + contractType: formData.contractType || null, + contractAmount: formData.contractAmount ? parseFloat(formData.contractAmount) : null, + + // 국내계약 전용 필드들 + contractCounterparty: formData.contractCounterparty || null, + counterpartyType: formData.counterpartyType || null, + contractPeriod: formData.contractPeriod || null, + + // 자문 관련 필드 + factualRelation: formData.factualRelation || null, + + // 해외 관련 필드들 + projectNumber: formData.projectNumber || null, + shipownerOrderer: formData.shipownerOrderer || null, + governingLaw: formData.governingLaw || null, + projectType: formData.projectType || null, + }) + .returning() + + console.log(`📋 검토요청 정보 저장 완료: ${createdRequest.reviewDepartment}`) + + // 3. 첨부파일 처리 + const uploadedFiles = [] + const failedFiles = [] + + if (attachments.length > 0) { + console.log(`📎 첨부파일 처리 시작: ${attachments.length}개`) + + for (const file of attachments) { + try { + const uploadResult = await uploadAttachment(file, legalWorkId, userId) + + // DB에 첨부파일 정보 저장 + const [attachmentRecord] = await tx + .insert(legalWorkAttachments) + .values({ + legalWorkId: legalWorkId, + fileName: uploadResult.fileName, + originalFileName: uploadResult.originalFileName, + filePath: uploadResult.filePath, + fileSize: uploadResult.fileSize, + mimeType: uploadResult.mimeType, + attachmentType: 'request', + isAutoGenerated: false, + }) + .returning() + + uploadedFiles.push({ + id: attachmentRecord.id, + name: uploadResult.originalFileName, + size: uploadResult.fileSize, + securityChecks: uploadResult.securityChecks + }) + + } catch (fileError) { + console.error(`❌ 파일 업로드 실패: ${file.name}`, fileError) + failedFiles.push({ + name: file.name, + error: fileError instanceof Error ? fileError.message : "업로드 실패" + }) + } + } + + console.log(`✅ 파일 업로드 완료: 성공 ${uploadedFiles.length}개, 실패 ${failedFiles.length}개`) + } + + return { + updatedWork, + createdRequest, + uploadedFiles, + failedFiles, + totalFiles: attachments.length, + } + }) + + // 페이지 재검증 + revalidatePath("/legal-works") + + // 성공 메시지 구성 + let message = `검토요청이 성공적으로 발송되었습니다.` + + if (result.totalFiles > 0) { + message += ` (첨부파일: 성공 ${result.uploadedFiles.length}개` + if (result.failedFiles.length > 0) { + message += `, 실패 ${result.failedFiles.length}개` + } + message += `)` + } + + console.log(`🎉 검토요청 처리 완료 - 법무업무 #${legalWorkId}`) + + return { + success: true, + data: { + message, + legalWorkId: legalWorkId, + requestId: result.createdRequest.id, + uploadedFiles: result.uploadedFiles, + failedFiles: result.failedFiles, + } + } + + } catch (error) { + console.error(`💥 검토요청 처리 중 오류 - 법무업무 #${legalWorkId}:`, error) + + return { + success: false, + error: error instanceof Error ? error.message : "검토요청 처리 중 오류가 발생했습니다." + } + } +} + + +// FormData를 사용하는 버전 (파일 업로드용) +export async function requestReviewWithFiles(formData: FormData) { + try { + // 기본 데이터 추출 + const legalWorkId = parseInt(formData.get("legalWorkId") as string) + + const requestData: RequestReviewData = { + dueDate: formData.get("dueDate") as string, + assignee: formData.get("assignee") as string || undefined, + notificationMethod: formData.get("notificationMethod") as "email" | "internal" | "both", + reviewDepartment: formData.get("reviewDepartment") as "준법문의" | "법무검토", + inquiryType: formData.get("inquiryType") as "국내계약" | "국내자문" | "해외계약" | "해외자문" || undefined, + title: formData.get("title") as string, + requestContent: formData.get("requestContent") as string, + isPublic: formData.get("isPublic") === "true", + + // 법무검토 관련 필드들 + contractProjectName: formData.get("contractProjectName") as string || undefined, + contractType: formData.get("contractType") as string || undefined, + contractCounterparty: formData.get("contractCounterparty") as string || undefined, + counterpartyType: formData.get("counterpartyType") as "법인" | "개인" || undefined, + contractPeriod: formData.get("contractPeriod") as string || undefined, + contractAmount: formData.get("contractAmount") as string || undefined, + factualRelation: formData.get("factualRelation") as string || undefined, + projectNumber: formData.get("projectNumber") as string || undefined, + shipownerOrderer: formData.get("shipownerOrderer") as string || undefined, + projectType: formData.get("projectType") as string || undefined, + governingLaw: formData.get("governingLaw") as string || undefined, + } + + // 첨부파일 추출 + const attachments: File[] = [] + for (const [key, value] of formData.entries()) { + if (key.startsWith("attachment_") && value instanceof File && value.size > 0) { + attachments.push(value) + } + } + + return await requestReview(legalWorkId, requestData, attachments) + + } catch (error) { + console.error("FormData 처리 중 오류:", error) + return { + success: false, + error: "요청 데이터 처리 중 오류가 발생했습니다." + } + } +} + +// 검토요청 가능 여부 확인 +export async function canRequestReview(legalWorkId: number) { + try { + const [work] = await db + .select({ status: legalWorks.status }) + .from(legalWorks) + .where(eq(legalWorks.id, legalWorkId)) + .limit(1) + + if (!work) { + return { canRequest: false, reason: "법무업무를 찾을 수 없습니다." } + } + + if (work.status !== "신규등록") { + return { + canRequest: false, + reason: `현재 상태(${work.status})에서는 검토요청을 할 수 없습니다. 신규등록 상태에서만 가능합니다.` + } + } + + return { canRequest: true } + + } catch (error) { + console.error("검토요청 가능 여부 확인 중 오류:", error) + return { + canRequest: false, + reason: "상태 확인 중 오류가 발생했습니다." + } + } +} + +// 삭제 요청 타입 +interface RemoveLegalWorksInput { + ids: number[] +} + +// 응답 타입 +interface RemoveLegalWorksResponse { + error?: string + success?: boolean +} + +/** + * 법무업무 삭제 서버 액션 + */ +export async function removeLegalWorks({ + ids, +}: RemoveLegalWorksInput): Promise<RemoveLegalWorksResponse> { + try { + // 유효성 검사 + if (!ids || ids.length === 0) { + return { + error: "삭제할 법무업무를 선택해주세요.", + } + } + + // 삭제 가능한 상태인지 확인 (선택적) + const existingWorks = await db + .select({ id: legalWorks.id, status: legalWorks.status }) + .from(legalWorks) + .where(inArray(legalWorks.id, ids)) + + // 삭제 불가능한 상태 체크 (예: 진행중인 업무는 삭제 불가) + const nonDeletableWorks = existingWorks.filter( + work => work.status === "검토중" || work.status === "담당자배정" + ) + + if (nonDeletableWorks.length > 0) { + return { + error: "진행중인 법무업무는 삭제할 수 없습니다.", + } + } + + // 실제 삭제 실행 + const result = await db + .delete(legalWorks) + .where(inArray(legalWorks.id, ids)) + + // 결과 확인 + if (result.changes === 0) { + return { + error: "삭제할 법무업무를 찾을 수 없습니다.", + } + } + + // 캐시 재검증 + revalidatePath("/legal-works") // 실제 경로에 맞게 수정 + + return { + success: true, + } + + } catch (error) { + console.error("법무업무 삭제 중 오류 발생:", error) + + return { + error: "법무업무 삭제 중 오류가 발생했습니다. 다시 시도해주세요.", + } + } +} + +/** + * 단일 법무업무 삭제 (선택적) + */ +export async function removeLegalWork(id: number): Promise<RemoveLegalWorksResponse> { + return removeLegalWorks({ ids: [id] }) +}
\ No newline at end of file |
